Canvas: Generate graph state out of dodo-app config
Restructure code, create shared config lib.
Change-Id: I2cf06d35c486d4557484daf8618a2c215316fa7e
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index 513f1f0..39db5b4 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -1,235 +1,5 @@
-import { AppNode, Env, Message, MessageType, NodeType, ServiceType, VolumeType } from "./state";
-
-export type AuthDisabled = {
- enabled: false;
-};
-
-export type AuthEnabled = {
- enabled: true;
- groups: string[];
- noAuthPathPatterns: string[];
-};
-
-export type Auth = AuthDisabled | AuthEnabled;
-
-export type Ingress = {
- network: string;
- subdomain: string;
- port: { name: string } | { value: string };
- auth: Auth;
-};
-
-export type Domain = {
- network: string;
- subdomain: string;
-};
-
-export type PortValue =
- | {
- name: string;
- }
- | {
- value: number;
- };
-
-export type PortDomain = Domain & {
- port: PortValue;
-};
-
-export type Service = {
- type: ServiceType;
- name: string;
- source: {
- repository: string;
- branch: string;
- rootDir: string;
- };
- ports?: {
- name: string;
- value: number;
- protocol: "TCP" | "UDP";
- }[];
- env?: {
- name: string;
- alias?: string;
- }[];
- ingress?: Ingress[];
- expose?: PortDomain[];
- volume?: string[];
- preBuildCommands?: { bin: string }[];
- dev?: {
- enabled: boolean;
- username?: string;
- ssh?: Domain;
- codeServer?: Domain;
- };
-};
-
-export type Volume = {
- name: string;
- accessMode: VolumeType;
- size: string;
-};
-
-export type PostgreSQL = {
- name: string;
- size: string;
- expose?: Domain[];
-};
-
-export type MongoDB = {
- name: string;
- size: string;
- expose?: Domain[];
-};
-
-export type Config = {
- input: {
- appId: string;
- managerAddr: string;
- };
- service?: Service[];
- volume?: Volume[];
- postgresql?: PostgreSQL[];
- mongodb?: MongoDB[];
-};
-
-export function generateDodoConfig(appId: string | undefined, nodes: AppNode[], env: Env): Config | null {
- try {
- if (appId == null || env.managerAddr == null) {
- return null;
- }
- const networkMap = new Map(env.networks.map((n) => [n.domain, n.name]));
- const ingressNodes = nodes
- .filter((n) => n.type === "gateway-https")
- .filter((n) => n.data.https !== undefined && !n.data.readonly);
- const tcpNodes = nodes
- .filter((n) => n.type === "gateway-tcp")
- .filter((n) => n.data.exposed !== undefined && !n.data.readonly);
- const findExpose = (n: AppNode): PortDomain[] => {
- return n.data.ports
- .map((p) => [n.id, p.id, p.name])
- .flatMap((sp) => {
- return tcpNodes.flatMap((i) =>
- (i.data.exposed || [])
- .filter((t) => t.serviceId === sp[0] && t.portId === sp[1])
- .map(() => ({
- network: networkMap.get(i.data.network!)!,
- subdomain: i.data.subdomain!,
- port: { name: sp[2] },
- })),
- );
- });
- };
- return {
- input: {
- appId: appId,
- managerAddr: env.managerAddr,
- },
- service: nodes
- .filter((n) => n.type === "app")
- .map((n): Service => {
- return {
- type: n.data.type,
- name: n.data.label,
- source: {
- repository: nodes
- .filter((i) => i.type === "github")
- .find((i) => i.id === n.data.repository.id)!.data.repository!.sshURL,
- branch: n.data.repository.branch,
- rootDir: n.data.repository.rootDir,
- },
- ports: (n.data.ports || [])
- .filter((p) => !n.data.dev?.enabled || (p.value != 22 && p.value != 9090))
- .map((p) => ({
- name: p.name,
- value: p.value,
- protocol: "TCP", // TODO(gio)
- })),
- env: (n.data.envVars || [])
- .filter((e) => "name" in e)
- .map((e) => ({
- name: e.name,
- alias: "alias" in e ? e.alias : undefined,
- })),
- ingress: ingressNodes
- .filter((i) => i.data.https!.serviceId === n.id)
- .map(
- (i): Ingress => ({
- network: networkMap.get(i.data.network!)!,
- subdomain: i.data.subdomain!,
- port: {
- name: n.data.ports.find((p) => p.id === i.data.https!.portId)!.name,
- },
- auth:
- i.data.auth?.enabled || false
- ? {
- enabled: true,
- groups: i.data.auth!.groups,
- noAuthPathPatterns: i.data.auth!.noAuthPathPatterns,
- }
- : {
- enabled: false,
- },
- }),
- ),
- expose: findExpose(n),
- preBuildCommands: n.data.preBuildCommands
- ? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))
- : [],
- dev: {
- enabled: n.data.dev ? n.data.dev.enabled : false,
- username: n.data.dev && n.data.dev.enabled ? env.user.username : undefined,
- codeServer:
- n.data.dev?.enabled && n.data.dev.expose != null
- ? {
- network: networkMap.get(n.data.dev.expose.network)!,
- subdomain: n.data.dev.expose.subdomain,
- }
- : undefined,
- ssh:
- n.data.dev?.enabled && n.data.dev.expose != null
- ? {
- network: networkMap.get(n.data.dev.expose.network)!,
- subdomain: n.data.dev.expose.subdomain,
- }
- : undefined,
- },
- };
- }),
- volume: nodes
- .filter((n) => n.type === "volume")
- .map(
- (n): Volume => ({
- name: n.data.label,
- accessMode: n.data.type,
- size: n.data.size,
- }),
- ),
- postgresql: nodes
- .filter((n) => n.type === "postgresql")
- .map(
- (n): PostgreSQL => ({
- name: n.data.label,
- size: "1Gi", // TODO(gio)
- expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
- }),
- ),
- mongodb: nodes
- .filter((n) => n.type === "mongodb")
- .map(
- (n): MongoDB => ({
- name: n.data.label,
- size: "1Gi", // TODO(gio)
- expose: findExpose(n).map((e) => ({ network: e.network, subdomain: e.subdomain })),
- }),
- ),
- };
- } catch (e) {
- console.log(e);
- return null;
- }
-}
+import { AppNode, NodeType } from "config";
+import { Message, MessageType } from "./state";
export interface Validator {
(nodes: AppNode[]): Message[];
@@ -347,7 +117,7 @@
}) satisfies Message,
);
const noApp = git
- .filter((n) => !nodes.some((i) => i.type === "app" && i.data?.repository?.id === n.id))
+ .filter((n) => !nodes.some((i) => i.type === "app" && i.data?.repository?.repoNodeId === n.id))
.map(
(n) =>
({
@@ -383,7 +153,7 @@
}),
);
const noSource = apps
- .filter((n) => n.data == null || n.data.repository == null || n.data.repository.id === "")
+ .filter((n) => n.data == null || n.data.repository == null || n.data.repository.repoNodeId === "")
.map(
(n): Message => ({
id: `${n.id}-no-repo`,
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index a0e4e91..8ada938 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -1,7 +1,7 @@
import { Category, defaultCategories } from "./categories";
import { CreateValidators, Validator } from "./config";
import { GitHubService, GitHubServiceImpl, GitHubRepository } from "./github";
-import type { Edge, Node, OnConnect, OnEdgesChange, OnNodesChange, Viewport as ReactFlowViewport } from "@xyflow/react";
+import type { Edge, OnConnect, OnEdgesChange, OnNodesChange, Viewport as ReactFlowViewport } from "@xyflow/react";
import {
addEdge,
applyEdgeChanges,
@@ -13,217 +13,8 @@
} from "@xyflow/react";
import type { DeepPartial } from "react-hook-form";
import { v4 as uuidv4 } from "uuid";
-import { z } from "zod";
import { create } from "zustand";
-
-export const serviceAnalyzisSchema = z.object({
- name: z.string(),
- location: z.string(),
- configVars: z.array(
- z.object({
- name: z.string(),
- category: z.enum(["CommandLineFlag", "EnvironmentVariable"]),
- type: z.optional(z.enum(["String", "Number", "Boolean"])),
- semanticType: z.optional(
- z.enum([
- "EXPANDED_ENV_VAR",
- "PORT",
- "FILESYSTEM_PATH",
- "DATABASE_URL",
- "SQLITE_PATH",
- "POSTGRES_URL",
- "POSTGRES_PASSWORD",
- "POSTGRES_USER",
- "POSTGRES_DB",
- "POSTGRES_PORT",
- "POSTGRES_HOST",
- "POSTGRES_SSL",
- "MONGO_URL",
- "MONGO_PASSWORD",
- "MONGO_USER",
- "MONGO_DB",
- "MONGO_PORT",
- "MONGO_HOST",
- "MONGO_SSL",
- ]),
- ),
- }),
- ),
-});
-
-export type InitData = {
- label: string;
- envVars: BoundEnvVar[];
- ports: Port[];
-};
-
-export type NodeData = InitData & {
- activeField?: string | undefined;
- state?: string | null;
-};
-
-export type PortConnectedTo = {
- serviceId: string;
- portId: string;
-};
-
-export type NetworkData = NodeData & {
- domain: string;
-};
-
-export type NetworkNode = Node<NetworkData> & {
- type: "network";
-};
-
-export type GatewayHttpsData = NodeData & {
- readonly?: boolean;
- network?: string;
- subdomain?: string;
- https?: PortConnectedTo;
- auth?: {
- enabled: boolean;
- groups: string[];
- noAuthPathPatterns: string[];
- };
-};
-
-export type GatewayHttpsNode = Node<GatewayHttpsData> & {
- type: "gateway-https";
-};
-
-export type GatewayTCPData = NodeData & {
- readonly?: boolean;
- network?: string;
- subdomain?: string;
- exposed: PortConnectedTo[];
- selected?: {
- serviceId?: string;
- portId?: string;
- };
-};
-
-export type GatewayTCPNode = Node<GatewayTCPData> & {
- type: "gateway-tcp";
-};
-
-export type Port = {
- id: string;
- name: string;
- value: number;
-};
-
-export const ServiceTypes = [
- "deno:2.2.0",
- "golang:1.20.0",
- "golang:1.22.0",
- "golang:1.24.0",
- "hugo:latest",
- "php:8.2-apache",
- "nextjs:deno-2.0.0",
- "nodejs:23.1.0",
- "nodejs:24.0.2",
-] as const;
-export type ServiceType = (typeof ServiceTypes)[number];
-
-export type Domain = {
- network: string;
- subdomain: string;
-};
-
-export type ServiceData = NodeData & {
- type: ServiceType;
- repository?:
- | {
- id: number;
- repoNodeId: string;
- }
- | {
- id: number;
- repoNodeId: string;
- branch: string;
- }
- | {
- id: number;
- repoNodeId: string;
- branch: string;
- rootDir: string;
- };
- env: string[];
- volume: string[];
- preBuildCommands: string;
- isChoosingPortToConnect: boolean;
- dev?:
- | {
- enabled: false;
- expose?: Domain;
- }
- | {
- enabled: true;
- expose?: Domain;
- codeServerNodeId: string;
- sshNodeId: string;
- };
- info?: z.infer<typeof serviceAnalyzisSchema>;
-};
-
-export type ServiceNode = Node<ServiceData> & {
- type: "app";
-};
-
-export type VolumeType = "ReadWriteOnce" | "ReadOnlyMany" | "ReadWriteMany" | "ReadWriteOncePod";
-
-export type VolumeData = NodeData & {
- type: VolumeType;
- size: string;
- attachedTo: string[];
-};
-
-export type VolumeNode = Node<VolumeData> & {
- type: "volume";
-};
-
-export type PostgreSQLData = NodeData & {
- volumeId: string;
-};
-
-export type PostgreSQLNode = Node<PostgreSQLData> & {
- type: "postgresql";
-};
-
-export type MongoDBData = NodeData & {
- volumeId: string;
-};
-
-export type MongoDBNode = Node<MongoDBData> & {
- type: "mongodb";
-};
-
-export type GithubData = NodeData & {
- repository?: {
- id: number;
- sshURL: string;
- fullName: string;
- };
-};
-
-export type GithubNode = Node<GithubData> & {
- type: "github";
-};
-
-export type NANode = Node<NodeData> & {
- type: undefined;
-};
-
-export type AppNode =
- | NetworkNode
- | GatewayHttpsNode
- | GatewayTCPNode
- | ServiceNode
- | VolumeNode
- | PostgreSQLNode
- | MongoDBNode
- | GithubNode
- | NANode;
+import { AppNode, Env, NodeType, VolumeNode, GatewayTCPData, envSchema } from "config";
export function nodeLabel(n: AppNode): string {
try {
@@ -319,38 +110,6 @@
}
}
-export type BoundEnvVar =
- | {
- id: string;
- source: string | null;
- }
- | {
- id: string;
- source: string | null;
- name: string;
- isEditting: boolean;
- }
- | {
- id: string;
- source: string | null;
- name: string;
- alias: string;
- isEditting: boolean;
- }
- | {
- id: string;
- source: string | null;
- portId: string;
- name: string;
- alias: string;
- isEditting: boolean;
- };
-
-export type EnvVar = {
- name: string;
- value: string;
-};
-
export function nodeEnvVarNamePort(n: AppNode, portName: string): string {
return `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS_${portName.toUpperCase()}`;
}
@@ -381,8 +140,6 @@
}
}
-export type NodeType = Exclude<Pick<AppNode, "type">["type"], undefined>;
-
export type MessageType = "INFO" | "WARNING" | "FATAL";
export type Message = {
@@ -395,100 +152,6 @@
onClick?: (state: AppState) => void;
};
-export const accessSchema = z.discriminatedUnion("type", [
- z.object({
- type: z.literal("https"),
- name: z.string(),
- address: z.string(),
- }),
- z.object({
- type: z.literal("ssh"),
- name: z.string(),
- host: z.string(),
- port: z.number(),
- }),
- z.object({
- type: z.literal("tcp"),
- name: z.string(),
- host: z.string(),
- port: z.number(),
- }),
- z.object({
- type: z.literal("udp"),
- name: z.string(),
- host: z.string(),
- port: z.number(),
- }),
- z.object({
- type: z.literal("postgresql"),
- name: z.string(),
- host: z.string(),
- port: z.number(),
- database: z.string(),
- username: z.string(),
- password: z.string(),
- }),
- z.object({
- type: z.literal("mongodb"),
- name: z.string(),
- host: z.string(),
- port: z.number(),
- database: z.string(),
- username: z.string(),
- password: z.string(),
- }),
-]);
-
-export const serviceInfoSchema = z.object({
- name: z.string(),
- workers: z.array(
- z.object({
- id: z.string(),
- commit: z.optional(
- z.object({
- hash: z.string(),
- message: z.string(),
- }),
- ),
- commands: z.optional(
- z.array(
- z.object({
- command: z.string(),
- state: z.string(),
- }),
- ),
- ),
- }),
- ),
-});
-
-export const envSchema = z.object({
- managerAddr: z.optional(z.string().min(1)),
- instanceId: z.optional(z.string().min(1)),
- deployKeyPublic: z.optional(z.nullable(z.string().min(1))),
- networks: z
- .array(
- z.object({
- name: z.string().min(1),
- domain: z.string().min(1),
- hasAuth: z.boolean(),
- }),
- )
- .default([]),
- integrations: z.object({
- github: z.boolean(),
- }),
- services: z.array(serviceInfoSchema),
- user: z.object({
- id: z.string(),
- username: z.string(),
- }),
- access: z.array(accessSchema),
-});
-
-export type ServiceInfo = z.infer<typeof serviceInfoSchema>;
-export type Env = z.infer<typeof envSchema>;
-
const defaultEnv: Env = {
managerAddr: undefined,
deployKeyPublic: undefined,
@@ -729,15 +392,15 @@
method: "GET",
});
const inst = await resp.json();
- setN(inst.nodes);
- set({ edges: inst.edges });
+ setN(inst.state.nodes);
+ set({ edges: inst.state.edges });
injectNetworkNodes();
if (
- get().zoom.x !== inst.viewport.x ||
- get().zoom.y !== inst.viewport.y ||
- get().zoom.zoom !== inst.viewport.zoom
+ get().zoom.x !== inst.state.viewport.x ||
+ get().zoom.y !== inst.state.viewport.y ||
+ get().zoom.zoom !== inst.state.viewport.zoom
) {
- set({ zoom: inst.viewport });
+ set({ zoom: inst.state.viewport });
}
};