Canvas: Form to choose source repository
Change-Id: I48011d6374e036ead934815ed8e88dc0d1bb914e
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index 3eea939..dcccd82 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -8,7 +8,7 @@
import { Form, FormControl, FormField, FormItem, FormMessage } from './ui/form';
import { Input } from './ui/input';
import { Button } from './ui/button';
-import { Handle, Position } from "@xyflow/react";
+import { Handle, Position, useNodes } from "@xyflow/react";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
import { PencilIcon, XIcon } from "lucide-react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
@@ -61,8 +61,15 @@
value: z.coerce.number().gt(0, "can not be negative"),
});
+const sourceSchema = z.object({
+ id: z.string().min(1, "required"),
+ branch: z.string(),
+ rootDir: z.string(),
+});
+
export function NodeAppDetails({ id, data }: ServiceNode) {
const store = useStateStore();
+ const nodes = useNodes();
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
mode: "onChange",
@@ -225,7 +232,6 @@
return true;
}
if (t.data.exposed.find((e) => e.serviceId === id && e.portId === portId)) {
- console.log(11111, e);
tcpRemoved.add(t.id);
return false;
}
@@ -276,6 +282,64 @@
preBuildCommands: e.currentTarget.value,
});
}, [id, store]);
+
+ const sourceForm = useForm<z.infer<typeof sourceSchema>>({
+ resolver: zodResolver(sourceSchema),
+ mode: "onChange",
+ defaultValues: {
+ id: data?.repository?.id,
+ branch: data.repository && "branch" in data.repository ? data.repository.branch : undefined,
+ rootDir: data.repository && "rootDir" in data.repository ? data.repository.rootDir : undefined,
+ },
+ });
+ useEffect(() => {
+ const sub = sourceForm.watch((value: DeepPartial<z.infer<typeof sourceSchema>>, { name }: { name?: keyof z.infer<typeof sourceSchema> | undefined, type?: EventType | undefined }) => {
+ console.log(value);
+ if (name === "id") {
+ let edges = store.edges;
+ if (data?.repository?.id !== undefined) {
+ edges = edges.filter((e) => {
+ if (e.target === id && e.targetHandle === "repository" && e.source === data.repository.id) {
+ return false;
+ } else {
+ return true;
+ }
+ });
+ }
+ if (value.id !== undefined) {
+ edges = edges.concat({
+ id: uuidv4(),
+ source: value.id,
+ sourceHandle: "repository",
+ target: id,
+ targetHandle: "repository",
+ });
+ }
+ store.setEdges(edges);
+ store.updateNodeData<"app">(id, {
+ repository: {
+ id: value.id,
+ },
+ });
+ } else if (name === "branch") {
+ store.updateNodeData<"app">(id, {
+ repository: {
+ ...data?.repository,
+ branch: value.branch,
+ },
+ });
+ } else if (name === "rootDir") {
+ store.updateNodeData<"app">(id, {
+ repository: {
+ ...data?.repository,
+ rootDir: value.rootDir,
+ },
+ });
+ }
+ });
+ return () => sub.unsubscribe();
+ }, [id, data, sourceForm, store]);
+
return (
<>
<Form {...form}>
@@ -315,6 +379,56 @@
/>
</form>
</Form>
+ Source
+ <Form {...sourceForm}>
+ <form className="space-y-2">
+ <FormField
+ control={sourceForm.control}
+ name="id"
+ render={({ field }) => (
+ <FormItem>
+ <Select onValueChange={field.onChange} defaultValue={field.value}>
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue placeholder="Repository" />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {nodes.filter((n) => n.type === "github" && n.data.address).map((n) => (
+ <SelectItem key={n.id} value={n.id}>{`${n.data.address!}`}</SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={sourceForm.control}
+ name="branch"
+ render={({ field }) => (
+ <FormItem>
+ <FormControl>
+ <Input placeholder="master" className="border border-black" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ <FormField
+ control={sourceForm.control}
+ name="rootDir"
+ render={({ field }) => (
+ <FormItem>
+ <FormControl>
+ <Input placeholder="/" className="border border-black" {...field} />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </form>
+ </Form>
Ports
<ul>
{data && data.ports && data.ports.map((p) => (<li key={p.id}><Button size={"icon"} variant={"ghost"} onClick={() => removePort(p.id)}><XIcon /></Button> {p.name} - {p.value}</li>))}
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index 3eed93e..22353f8 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -54,7 +54,7 @@
ingress?: Ingress[];
expose?: PortDomain[];
volume?: string[];
- preBuildCommands?: string[];
+ preBuildCommands?: { bin: string }[];
};
export type Volume = {
@@ -124,7 +124,7 @@
auth: { enabled: false },
})),
expose: findExpose(n),
- preBuildCommands: [n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))],
+ preBuildCommands: n.data.preBuildCommands ? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd })) : [],
};
}),
volume: nodes.filter((n) => n.type === "volume").map((n): Volume => ({
@@ -214,7 +214,7 @@
export function CreateValidators(): Validator {
return SortingValidator(
CombineValidators(
- EmptyValidator,
+ EmptyValidator,
GitRepositoryValidator,
ServiceValidator,
GatewayHTTPSValidator,
@@ -228,7 +228,7 @@
if (nodes.length > 0) {
return [];
}
- return [{
+ return [{
id: "no-nodes",
type: "FATAL",
message: "Start by importing application source code",
@@ -254,7 +254,7 @@
message: "Connect to service",
onHighlight: (store) => store.setHighlightCategory("Services", true),
onLooseHighlight: (store) => store.setHighlightCategory("Services", false),
-} satisfies Message));
+ } satisfies Message));
return noAddress.concat(noApp);
}
@@ -269,8 +269,8 @@
onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
onClick: (store) => {
store.updateNode(n.id, { selected: true });
- store.updateNodeData<"app">(n.id, {
- activeField: "name" ,
+ store.updateNodeData<"app">(n.id, {
+ activeField: "name",
});
},
}));
@@ -291,8 +291,8 @@
onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
onClick: (store) => {
store.updateNode(n.id, { selected: true });
- store.updateNodeData<"app">(n.id, {
- activeField: "type" ,
+ store.updateNodeData<"app">(n.id, {
+ activeField: "type",
});
},
}));
@@ -324,7 +324,7 @@
},
onLooseHighlight: (store) => {
store.updateNode(n.id, { selected: false });
- store.setHighlightCategory("gateways", false);
+ store.setHighlightCategory("gateways", false);
},
}));
});
@@ -339,7 +339,7 @@
nodeId: n.id,
message: `Can not expose same port using multiple ingresses: ${p.name} - ${p.value}`,
onHighlight: (store) => store.updateNode(n.id, { selected: true }),
- onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+ onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
};
})).filter((m) => m !== undefined);
return noName.concat(noSource).concat(noRuntime).concat(noPorts).concat(noIngress).concat(multipleIngress);
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index 664ba6e..ee6f6d2 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -74,7 +74,7 @@
"golang:1.24.0",
"hugo:latest",
"php:8.2-apache",
- "nextjs:deno-2.0.0",
+ "nextjs:deno-2.0.0",
"node-23.1.0"
] as const;
export type ServiceType = typeof ServiceTypes[number];
@@ -83,6 +83,11 @@
type: ServiceType;
repository: {
id: string;
+ } | {
+ id: string;
+ branch: string;
+ } | {
+ id: string;
branch: string;
rootDir: string;
};
@@ -173,11 +178,11 @@
return n.data !== undefined && n.data.ports !== undefined && n.data.ports.length > 0;
} else if (handle === "repository") {
if (!n.data || !n.data.repository || !n.data.repository.id) {
- return true;
+ return true;
}
return false;
}
- return false;
+ return false;
case "github":
if (n.data !== undefined && n.data.address) {
return true;
@@ -202,7 +207,7 @@
return false;
}
if (n.data.type === "ReadWriteOnce" || n.data.type === "ReadWriteOncePod") {
- return n.data.attachedTo === undefined || n.data.attachedTo.length === 0;
+ return n.data.attachedTo === undefined || n.data.attachedTo.length === 0;
}
return true;
case undefined: throw new Error("MUST NOT REACH!");
@@ -244,7 +249,7 @@
export function nodeEnvVarNames(n: AppNode): string[] {
switch (n.type) {
case "app": return [
- `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS`,
+ `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS`,
...(n.data.ports || []).map((p) => nodeEnvVarNamePort(n, p.name)),
];
case "github": return [];
@@ -371,16 +376,18 @@
const v: Validator = CreateValidators();
export const useStateStore = create<AppState>((set, get): AppState => {
- set({ env: {
- "deployKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPK58vMu0MwIzdZT+mqpIBkhl48p9+/YwDCZv7MgTesF",
- "networks": [{
- "name": "Public",
- "domain": "v1.dodo.cloud",
- }, {
- "name": "Private",
- "domain": "p.v1.dodo.cloud",
- }],
- }});
+ set({
+ env: {
+ "deployKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPK58vMu0MwIzdZT+mqpIBkhl48p9+/YwDCZv7MgTesF",
+ "networks": [{
+ "name": "Public",
+ "domain": "v1.dodo.cloud",
+ }, {
+ "name": "Private",
+ "domain": "p.v1.dodo.cloud",
+ }],
+ }
+ });
console.log(get().env);
const setN = (nodes: AppNode[]) => {
set({
@@ -390,18 +397,18 @@
};
function updateNodeData<T extends NodeType>(id: string, d: DeepPartial<(AppNode & (Pick<AppNode, "type"> | { type: T }))["data"]>): void {
setN(get().nodes.map((n) => {
- if (n.id !== id) {
- return n;
- }
- const nd = {
- ...n,
- data: {
- ...n.data,
- ...d,
- },
- };
- return nd;
- })
+ if (n.id !== id) {
+ return n;
+ }
+ const nd = {
+ ...n,
+ data: {
+ ...n.data,
+ ...d,
+ },
+ };
+ return nd;
+ })
);
};
function updateNode<T extends NodeType>(id: string, d: DeepPartial<(AppNode & (Pick<AppNode, "type"> | { type: T }))>): void {
@@ -429,7 +436,7 @@
updateNodeData<"gateway-https">(sn.id, {
network: tn.data.domain,
});
- }else if (sn.type === "gateway-tcp") {
+ } else if (sn.type === "gateway-tcp") {
updateNodeData<"gateway-tcp">(sn.id, {
network: tn.data.domain,
});
@@ -628,12 +635,12 @@
onConnect,
refreshEnv: async () => {
return get().env;
- const resp = await fetch("/env");
- if (!resp.ok) {
- throw new Error("failed to fetch env config");
- }
- set({ env: envSchema.parse(await resp.json()) });
- return get().env;
+ const resp = await fetch("/env");
+ if (!resp.ok) {
+ throw new Error("failed to fetch env config");
+ }
+ set({ env: envSchema.parse(await resp.json()) });
+ return get().env;
},
setProject: (projectId) => set({ projectId }),
setProjects: (projects) => set({ projects }),