wip: form update route

This commit is contained in:
Derock 2024-01-20 21:11:00 -05:00
parent 6dc69ce517
commit 3c60018ca2
No known key found for this signature in database
25 changed files with 237 additions and 78 deletions

View file

@ -29,6 +29,7 @@
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@t3-oss/env-nextjs": "^0.7.1",
"@tanstack/react-query": "^5.17.1",

View file

@ -41,6 +41,9 @@ dependencies:
'@radix-ui/react-slot':
specifier: ^1.0.2
version: 1.0.2(@types/react@18.2.46)(react@18.2.0)
'@radix-ui/react-switch':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-tabs':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
@ -1642,13 +1645,13 @@ packages:
/@radix-ui/number@1.0.1:
resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
dev: false
/@radix-ui/primitive@1.0.1:
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
dev: false
/@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0):
@ -1664,7 +1667,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.46
'@types/react-dom': 18.2.18
@ -1713,7 +1716,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
@ -1733,7 +1736,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@types/react': 18.2.46
react: 18.2.0
dev: false
@ -1747,7 +1750,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@types/react': 18.2.46
react: 18.2.0
dev: false
@ -1795,7 +1798,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@types/react': 18.2.46
react: 18.2.0
dev: false
@ -1813,7 +1816,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
@ -1861,7 +1864,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@types/react': 18.2.46
react: 18.2.0
dev: false
@ -1879,7 +1882,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.46)(react@18.2.0)
@ -1906,7 +1909,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@types/react': 18.2.46
react: 18.2.0
@ -1946,7 +1949,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0)
@ -1984,7 +1987,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@floating-ui/react-dom': 2.0.4(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0)
@ -2014,7 +2017,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.46
'@types/react-dom': 18.2.18
@ -2035,7 +2038,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@types/react': 18.2.46
@ -2057,7 +2060,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.46)(react@18.2.0)
'@types/react': 18.2.46
'@types/react-dom': 18.2.18
@ -2078,7 +2081,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0)
@ -2171,6 +2174,33 @@ packages:
react: 18.2.0
dev: false
/@radix-ui/react-switch@1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.8
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@radix-ui/react-use-previous': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@radix-ui/react-use-size': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@types/react': 18.2.46
'@types/react-dom': 18.2.18
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@radix-ui/react-tabs@1.0.4(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==}
peerDependencies:
@ -2208,7 +2238,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@types/react': 18.2.46
react: 18.2.0
dev: false
@ -2222,7 +2252,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@types/react': 18.2.46
react: 18.2.0
@ -2237,7 +2267,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@types/react': 18.2.46
react: 18.2.0
@ -2252,7 +2282,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@types/react': 18.2.46
react: 18.2.0
dev: false
@ -2266,7 +2296,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@types/react': 18.2.46
react: 18.2.0
dev: false
@ -2280,7 +2310,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/rect': 1.0.1
'@types/react': 18.2.46
react: 18.2.0
@ -2295,7 +2325,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.46)(react@18.2.0)
'@types/react': 18.2.46
react: 18.2.0
@ -2314,7 +2344,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.18)(@types/react@18.2.46)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.46
'@types/react-dom': 18.2.18
@ -2325,7 +2355,7 @@ packages:
/@radix-ui/rect@1.0.1:
resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==}
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
dev: false
/@replit/codemirror-lang-csharp@6.2.0(@codemirror/autocomplete@6.12.0)(@codemirror/language@6.10.0)(@codemirror/state@6.4.0)(@codemirror/view@6.23.0)(@lezer/common@1.2.1)(@lezer/highlight@1.2.0)(@lezer/lr@1.3.14):
@ -4143,7 +4173,7 @@ packages:
/dom-helpers@3.4.0:
resolution: {integrity: sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==}
dependencies:
'@babel/runtime': 7.23.7
'@babel/runtime': 7.23.8
dev: false
/dotenv@16.3.1:

View file

@ -1,21 +0,0 @@
"use client";
import { api } from "~/trpc/react";
import { useProject } from "../../../_context/ProjectContext";
/**
* Returns detailed information about a service (or the one currently navigated to)
*/
export function useService(serviceId?: string) {
const project = useProject();
serviceId ??= project?.selectedService?.id;
if (!serviceId) {
throw new Error("No service ID provided");
}
return api.projects.services.get.useQuery({
projectId: project.id,
serviceId,
});
}

View file

@ -1,9 +0,0 @@
import DeploymentSettings from "./DeploymentSettings";
export default function AdvancedSettings() {
return (
<div>
<DeploymentSettings />
</div>
);
}

View file

@ -16,7 +16,7 @@ export function ProjectLayout(props: {
children: React.ReactNode;
}) {
const params = useParams();
const projectPath = `/project/${params.id as string}`;
const projectPath = `/project/${params.projectId as string}`;
const servicePath = `${projectPath}/service/${params.serviceId as string}`;
const project = api.projects.get.useQuery(

View file

@ -2,10 +2,12 @@ import { api } from "~/trpc/server";
import { ProjectLayout } from "./ProjectLayout";
export default async function ProjectPage(props: {
params: { id: string };
params: { projectId: string };
children: React.ReactNode;
}) {
const project = await api.projects.get.query({ projectId: props.params.id });
const project = await api.projects.get.query({
projectId: props.params.projectId,
});
return <ProjectLayout project={project}>{props.children}</ProjectLayout>;
}

View file

@ -0,0 +1,39 @@
"use client";
import { use } from "react";
import { api } from "~/trpc/react";
import { type RouterOutputs } from "~/trpc/shared";
import { type Awaitable } from "~/utils/utils";
import { useProject } from "../../../_context/ProjectContext";
/**
* Returns detailed information about a service (or the one currently navigated to)
*/
export function useService(
serviceId?: string,
defaultData?: Awaitable<RouterOutputs["projects"]["services"]["get"]>,
) {
const project = useProject();
serviceId ??= project?.selectedService?.id;
if (!serviceId) {
throw new Error("No service ID provided");
}
return api.projects.services.get.useQuery(
{
projectId: project.id,
serviceId,
},
{
initialData:
// use(
// apiServer.projects.services.get.query({
// projectId: project.id,
// serviceId: serviceId,
// })
// ),
defaultData instanceof Promise ? use(defaultData) : defaultData,
},
);
}

View file

@ -17,12 +17,13 @@ import {
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import { Switch } from "~/components/ui/switch";
import { FormSubmit, SimpleFormField, useForm } from "~/hooks/forms";
import { DOCKER_DEPLOY_MODE_MAP, DockerDeployMode } from "~/server/db/types";
import { useService } from "../_hooks/service";
import { type RouterOutputs } from "~/trpc/shared";
const formValidator = z.object({
replicas: z.number().int().positive(),
replicas: z.coerce.number().int().positive(),
maxReplicasPerNode: z.number().int().positive().nullable(),
deployMode: z.enum(["replicated", "global"]).nullable(),
zeroDowntime: z.boolean().nullable(),
@ -30,16 +31,19 @@ const formValidator = z.object({
command: z.string().optional().nullable(),
});
export default function DeploymentSettings() {
const { data: service } = useService();
export default function DeploymentSettings({
service,
}: {
service: RouterOutputs["projects"]["services"]["get"];
}) {
const form = useForm(formValidator, {
defaultValues: {
replicas: service?.replicas,
maxReplicasPerNode: service?.maxReplicasPerNode,
deployMode: service?.deployMode,
zeroDowntime: service?.zeroDowntime,
entrypoint: service?.entrypoint,
command: service?.command,
replicas: service.replicas,
maxReplicasPerNode: service.maxReplicasPerNode,
deployMode: service.deployMode,
zeroDowntime: service.zeroDowntime,
entrypoint: service.entrypoint,
command: service.command,
},
});
@ -127,6 +131,7 @@ export default function DeploymentSettings() {
name="zeroDowntime"
friendlyName="Zero Downtime"
description="When enabled, old containers will stay running until the new containers are online, alowing for zero-downtime deployments."
render={({ field }) => <Switch {...field} className="!my-4 block" />}
/>
<FormSubmit form={form} className="col-span-2" />

View file

@ -0,0 +1,17 @@
import { api } from "~/trpc/server";
import DeploymentSettings from "./DeploymentSettings";
export default async function AdvancedSettings(props: {
params: { projectId: string; serviceId: string };
}) {
const service = await api.projects.services.get.query({
serviceId: props.params.serviceId,
projectId: props.params.projectId,
});
return (
<div>
<DeploymentSettings service={service} />
</div>
);
}

View file

@ -0,0 +1,29 @@
"use client";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import * as React from "react";
import { cn } from "~/utils/utils";
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className,
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0",
)}
/>
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };

View file

@ -37,10 +37,12 @@ export function useForm<TSchema extends z.Schema<any>, TContext = any>(
schema: TSchema,
props: UseFormProps<z.infer<TSchema>, TContext> = {},
) {
return useFormHook<z.infer<TSchema>>({
const form = useFormHook<z.infer<TSchema>>({
...props,
resolver: zodResolver(schema),
});
return form;
}
/**

View file

@ -7,7 +7,13 @@ import { projectMiddleware } from "~/server/api/middleware/project";
import { serviceMiddleware } from "~/server/api/middleware/service";
import { authenticatedProcedure, createTRPCRouter } from "~/server/api/trpc";
import { service } from "~/server/db/schema";
import { DOCKER_DEPLOY_MODE_MAP, ServiceSource } from "~/server/db/types";
import {
DOCKER_DEPLOY_MODE_MAP,
DockerDeployMode,
DockerRestartCondition,
ServiceBuildMethod,
ServiceSource,
} from "~/server/db/types";
import { zDockerName } from "~/server/utils/zod";
import { getServiceContainers } from "./containers";
@ -32,7 +38,6 @@ export const serviceRouter = createTRPCRouter({
domains: true,
ports: true,
volumes: true,
project: true,
sysctls: true,
ulimits: true,
},
@ -47,6 +52,63 @@ export const serviceRouter = createTRPCRouter({
};
}),
update: authenticatedProcedure
.meta({
openapi: {
method: "PATCH",
path: "/api/projects/:projectId/services/:serviceId",
summary: "Update service",
},
})
.input(
z
.object({
projectId: z.string(),
serviceId: z.string(),
})
.merge(
z
.object({
source: z.nativeEnum(ServiceSource),
environment: z.string(),
dockerImage: z.string(),
dockerRegistryUsername: z.string(),
dockerRegistryPassword: z.string(),
// TODO: restrict to valid github url
githubUrl: z.string().url(),
githubBranch: z.string(),
gitUrl: z.string(),
gitBranch: z.string(),
buildMethod: z.nativeEnum(ServiceBuildMethod),
buildPath: z.string(),
command: z.string(),
entrypoint: z.string(),
replicas: z.number(),
maxReplicasPerNode: z.number(),
deployMode: z.nativeEnum(DockerDeployMode),
zeroDowntime: z.boolean(),
max_cpu: z.number(),
max_memory: z.string(),
max_pids: z.number(),
restart: z.nativeEnum(DockerRestartCondition),
})
.partial(),
),
)
.use(projectMiddleware)
.use(serviceMiddleware)
.mutation(async ({ ctx, input }) => {
await ctx.db
.update(service)
.set({
name: input.name,
zeroDowntime: input.zeroDowntime ? 1 : 0,
deployMode: input.deployMode,
})
.where(eq(service.id, ctx.service.id))
.execute();
}),
create: authenticatedProcedure
.meta({
openapi: {

View file

@ -156,7 +156,7 @@ export const service = sqliteTable(
// for github/git
buildMethod: integer("build_method").$type<ServiceBuildMethod>(),
buildPath: text("build_path"),
buildPath: text("build_path").default("/").notNull(),
// deployment settings
command: text("command"),

View file

@ -1,6 +1,8 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export type Awaitable<T> = T | Promise<T>;
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
return twMerge(clsx(inputs));
}