-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from Trugamr/develop
Create basic deployment builder form
- Loading branch information
Showing
21 changed files
with
763 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { getHintUtils } from '@epic-web/client-hints' | ||
import { | ||
clientHint as colorSchemeHint, | ||
subscribeToSchemeChange, | ||
} from '@epic-web/client-hints/color-scheme' | ||
import { clientHint as timeZoneHint } from '@epic-web/client-hints/time-zone' | ||
import { clientHint as reducedMotionHint } from '@epic-web/client-hints/reduced-motion' | ||
import { useRevalidator } from '@remix-run/react' | ||
import { useEffect } from 'react' | ||
|
||
const hintsUtils = getHintUtils({ | ||
theme: { | ||
...colorSchemeHint, | ||
cookieName: 'color-scheme', | ||
}, | ||
timeZone: { | ||
...timeZoneHint, | ||
cookieName: 'time-zone', | ||
}, | ||
reducedMotion: { | ||
...reducedMotionHint, | ||
cookieName: 'reduced-motion', | ||
}, | ||
}) | ||
|
||
export const { getHints } = hintsUtils | ||
|
||
export function ClientHintCheck() { | ||
const { revalidate } = useRevalidator() | ||
|
||
useEffect(() => { | ||
subscribeToSchemeChange(() => { | ||
revalidate() | ||
}) | ||
}, [revalidate]) | ||
|
||
return ( | ||
<script | ||
dangerouslySetInnerHTML={{ | ||
__html: hintsUtils.getClientHintCheckScript(), | ||
}} | ||
/> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import { Button } from './ui/button' | ||
import { THEME_FETCHER_KEY, Theme } from '~/lib/theme' | ||
import { useFetcher } from '@remix-run/react' | ||
import { P, match } from 'ts-pattern' | ||
import { LaptopIcon, MoonStarIcon, SunIcon } from 'lucide-react' | ||
import { useGlobalInfo } from '~/lib/hooks/use-global-info' | ||
import { useOptimisticTheme } from '~/lib/hooks/use-theme' | ||
|
||
type ThemeSwitcherOptions = { | ||
className?: string | ||
} | ||
|
||
export function ThemeSwitcher({ className }: ThemeSwitcherOptions) { | ||
const { preferences } = useGlobalInfo() | ||
|
||
const fetcher = useFetcher({ | ||
key: THEME_FETCHER_KEY, | ||
}) | ||
|
||
const optimistic = useOptimisticTheme() | ||
|
||
const next = match(optimistic ?? preferences.theme ?? 'system') | ||
.with('system', () => 'light' as const) | ||
.with(P.union(Theme.Light, 'light'), () => 'dark' as const) | ||
.with(P.union(Theme.Dark, 'dark'), () => 'system' as const) | ||
.exhaustive() | ||
|
||
return ( | ||
<fetcher.Form className={className} method="POST" action="/api/theme"> | ||
<Button name="theme" value={next} size="icon" variant="ghost"> | ||
{match(next) | ||
.with('system', () => <LaptopIcon />) | ||
.with(P.union(Theme.Light, 'light'), () => <SunIcon />) | ||
.with(P.union(Theme.Dark, 'dark'), () => <MoonStarIcon />) | ||
.exhaustive()} | ||
</Button> | ||
</fetcher.Form> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { Deployment } from '~/routes/deploy/schema' | ||
import { | ||
convertComposeConfigToYaml, | ||
convertDeploymentToComposeConfig, | ||
} from '~/routes/deploy/utils' | ||
import { getManagedStacksDirectory } from './stack.server' | ||
import path from 'node:path' | ||
import fs from 'node:fs/promises' | ||
|
||
export async function createNewDeployment(deployment: Deployment) { | ||
let composeConfig: ReturnType<typeof convertDeploymentToComposeConfig> | ||
try { | ||
composeConfig = convertDeploymentToComposeConfig(deployment) | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.error(error) | ||
throw new Error('Failed to convert deployment to compose config') | ||
} | ||
|
||
let composeYaml: ReturnType<typeof convertComposeConfigToYaml> | ||
try { | ||
composeYaml = convertComposeConfigToYaml(composeConfig) | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.error(error) | ||
throw new Error('Failed to convert compose config to yaml') | ||
} | ||
|
||
// Create a new directory for the stack | ||
const stackDirectory = path.resolve(getManagedStacksDirectory(), deployment.name) | ||
|
||
try { | ||
await fs.mkdir(stackDirectory) | ||
} catch (error) { | ||
if (error instanceof Error && 'code' in error && error.code === 'EEXIST') { | ||
throw new Error(`Stack with name "${deployment.name}" already exists`) | ||
} | ||
// eslint-disable-next-line no-console | ||
console.error(error) | ||
throw new Error('Failed to create stack directory') | ||
} | ||
|
||
// Create a new docker-compose.yml file | ||
const composeYamlPath = path.resolve(stackDirectory, 'docker-compose.yml') | ||
try { | ||
await fs.writeFile(composeYamlPath, composeYaml) | ||
} catch (error) { | ||
// eslint-disable-next-line no-console | ||
console.error(error) | ||
throw new Error('Failed to create docker-compose.yml file') | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { invariant } from '@epic-web/invariant' | ||
import { useRouteLoaderData } from '@remix-run/react' | ||
import type { loader as rootLoader } from '~/root' | ||
|
||
/** | ||
* Get global info from the loader in root | ||
*/ | ||
export function useGlobalInfo() { | ||
const data = useRouteLoaderData<typeof rootLoader>('root') | ||
invariant(data?.info, 'No global info found in root loader') | ||
|
||
return data.info | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { invariant } from '@epic-web/invariant' | ||
import { THEME_FETCHER_KEY, ThemeFormSchema, isTheme } from '../theme' | ||
import { useGlobalInfo } from './use-global-info' | ||
import { useFetcher } from '@remix-run/react' | ||
import { parse } from '@conform-to/zod' | ||
|
||
export function useTheme() { | ||
const { preferences, hints } = useGlobalInfo() | ||
invariant(isTheme(hints.theme), "Hints theme isn't a valid theme") | ||
|
||
const optimistic = useOptimisticTheme() | ||
if (optimistic === 'system') { | ||
return hints.theme | ||
} | ||
|
||
const theme = optimistic ?? preferences.theme ?? hints.theme | ||
invariant(isTheme(theme), "Theme isn't a valid theme") | ||
|
||
return theme | ||
} | ||
|
||
export function useOptimisticTheme() { | ||
const themeFetcher = useFetcher({ key: THEME_FETCHER_KEY }) | ||
|
||
if (themeFetcher.formData) { | ||
const submission = parse(themeFetcher.formData, { | ||
schema: ThemeFormSchema, | ||
}) | ||
|
||
return submission.value?.theme | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { createCookie } from '@remix-run/node' | ||
import { isTheme, ThemeFormSchema } from '~/lib/theme' | ||
import { addYears } from 'date-fns' | ||
import { z } from 'zod' | ||
|
||
const themeCookieName = 'theme' | ||
|
||
export async function getTheme(request: Request) { | ||
const cookieHeader = request.headers.get('Cookie') | ||
const themeCookie = createCookie(themeCookieName) | ||
const incomingTheme = await themeCookie.parse(cookieHeader) | ||
|
||
if (isTheme(incomingTheme)) { | ||
return incomingTheme | ||
} | ||
|
||
return null | ||
} | ||
|
||
export async function getThemeCookie(theme: z.infer<typeof ThemeFormSchema>['theme']) { | ||
const themeCookie = createCookie(themeCookieName) | ||
|
||
return await themeCookie.serialize(theme === 'system' ? null : theme, { | ||
path: '/', | ||
expires: addYears(new Date(), 1), | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { z } from 'zod' | ||
|
||
export enum Theme { | ||
Light = 'light', | ||
Dark = 'dark', | ||
} | ||
|
||
const themes = Object.values<Theme>(Theme) | ||
|
||
export const THEME_FETCHER_KEY = 'THEME_FETCHER' | ||
|
||
export const ThemeFormSchema = z.object({ | ||
theme: z.enum(['light', 'dark', 'system']), | ||
}) | ||
|
||
/** | ||
* Narrow down the type of a string to a Theme | ||
*/ | ||
export function isTheme(value: unknown): value is Theme { | ||
return typeof value === 'string' && themes.includes(value as Theme) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.