Canvas: Prettier
Change-Id: I620dde109df0f29f0c85c6fe150e347d2c32a03e
diff --git a/apps/canvas/front/src/lib/categories.ts b/apps/canvas/front/src/lib/categories.ts
index 17e786e..8753455 100644
--- a/apps/canvas/front/src/lib/categories.ts
+++ b/apps/canvas/front/src/lib/categories.ts
@@ -1,101 +1,105 @@
import { NodeType, InitData } from "@/lib/state";
export interface CategoryItem<T extends NodeType> {
- title: string;
- init: InitData;
- type: T;
-};
+ title: string;
+ init: InitData;
+ type: T;
+}
export type Category = {
- title: string;
- items: CategoryItem<NodeType>[];
- active?: boolean;
+ title: string;
+ items: CategoryItem<NodeType>[];
+ active?: boolean;
};
const defaultInit: Pick<InitData, "label" | "envVars" | "ports"> = {
- label: "",
- envVars: [],
- ports: [],
+ label: "",
+ envVars: [],
+ ports: [],
};
export const defaultCategories: Category[] = [
- {
- title: "Repository",
- items: [
- {
- title: "Github",
- init: {
- ...defaultInit,
- },
- type: "github",
- }
- ]
- },
- {
- title: "Services",
- items: [
- {
- title: "Service",
- init: {
- ...defaultInit,
- },
- type: "app",
- }
- ],
- },
- {
- title: "Storage",
- items: [
- {
- title: "File system",
- init: {
- ...defaultInit,
- },
- type: "volume",
- },
- {
- title: "PostgreSQL",
- init: {
- ...defaultInit,
- ports: [{
- id: "connection",
- name: "connection",
- value: 5432,
- }],
- },
- type: "postgresql",
- },
- {
- title: "MongoDB",
- init: {
- ...defaultInit,
- ports: [{
- id: "connection",
- name: "connection",
- value: 27017,
- }],
- },
- type: "mongodb",
- },
- ],
- },
- {
- title: "Gateways",
- items: [
- {
- title: "HTTPS",
- init: {
- ...defaultInit,
- },
- type: "gateway-https",
- },
- {
- title: "TCP",
- init: {
- ...defaultInit,
- },
- type: "gateway-tcp",
- },
- ],
- },
-];
\ No newline at end of file
+ {
+ title: "Repository",
+ items: [
+ {
+ title: "Github",
+ init: {
+ ...defaultInit,
+ },
+ type: "github",
+ },
+ ],
+ },
+ {
+ title: "Services",
+ items: [
+ {
+ title: "Service",
+ init: {
+ ...defaultInit,
+ },
+ type: "app",
+ },
+ ],
+ },
+ {
+ title: "Storage",
+ items: [
+ {
+ title: "File system",
+ init: {
+ ...defaultInit,
+ },
+ type: "volume",
+ },
+ {
+ title: "PostgreSQL",
+ init: {
+ ...defaultInit,
+ ports: [
+ {
+ id: "connection",
+ name: "connection",
+ value: 5432,
+ },
+ ],
+ },
+ type: "postgresql",
+ },
+ {
+ title: "MongoDB",
+ init: {
+ ...defaultInit,
+ ports: [
+ {
+ id: "connection",
+ name: "connection",
+ value: 27017,
+ },
+ ],
+ },
+ type: "mongodb",
+ },
+ ],
+ },
+ {
+ title: "Gateways",
+ items: [
+ {
+ title: "HTTPS",
+ init: {
+ ...defaultInit,
+ },
+ type: "gateway-https",
+ },
+ {
+ title: "TCP",
+ init: {
+ ...defaultInit,
+ },
+ type: "gateway-tcp",
+ },
+ ],
+ },
+];
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index 3ea29b6..ea7f892 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -1,394 +1,525 @@
import { AppNode, Env, GatewayHttpsNode, Message, MessageType, NodeType, ServiceType, VolumeType } from "./state";
export type AuthDisabled = {
- enabled: false;
+ enabled: false;
};
export type AuthEnabled = {
- enabled: true;
- groups: string[];
- noAuthPathPatterns: string[];
+ 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;
+ network: string;
+ subdomain: string;
+ port: { name: string } | { value: string };
+ auth: Auth;
};
export type Domain = {
- network: string;
- subdomain: string;
+ network: string;
+ subdomain: string;
};
-export type PortValue = {
- name: string;
-} | {
- value: number;
-};
+export type PortValue =
+ | {
+ name: string;
+ }
+ | {
+ value: number;
+ };
export type PortDomain = Domain & {
- port: PortValue;
-}
+ 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 }[];
+ 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 }[];
};
export type Volume = {
- name: string;
- accessMode: VolumeType;
- size: string;
+ name: string;
+ accessMode: VolumeType;
+ size: string;
};
export type PostgreSQL = {
- name: string;
- size: string;
- expose?: Domain[];
+ name: string;
+ size: string;
+ expose?: Domain[];
};
export type MongoDB = {
- name: string;
- size: string;
- expose?: Domain[];
+ name: string;
+ size: string;
+ expose?: Domain[];
};
export type Config = {
- service?: Service[];
- volume?: Volume[];
- postgresql?: PostgreSQL[];
- mongodb?: MongoDB[];
+ service?: Service[];
+ volume?: Volume[];
+ postgresql?: PostgreSQL[];
+ mongodb?: MongoDB[];
};
export function generateDodoConfig(nodes: AppNode[], env: Env): Config | null {
- try {
- 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);
- const tcpNodes = nodes.filter((n) => n.type === "gateway-tcp").filter((n) => n.data.exposed !== undefined);
- 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 {
- 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 || []).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: GatewayHttpsNode): 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 })) : [],
- };
- }),
- 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;
- }
+ try {
+ 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);
+ const tcpNodes = nodes.filter((n) => n.type === "gateway-tcp").filter((n) => n.data.exposed !== undefined);
+ 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 {
+ 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 || []).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: GatewayHttpsNode): 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 }))
+ : [],
+ };
+ }),
+ 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;
+ }
}
export interface Validator {
- (nodes: AppNode[]): Message[];
+ (nodes: AppNode[]): Message[];
}
function CombineValidators(...v: Validator[]): Validator {
- return (n) => v.flatMap((v) => v(n));
+ return (n) => v.flatMap((v) => v(n));
}
function MessageTypeToNumber(t: MessageType) {
- switch (t) {
- case "FATAL": return 0;
- case "WARNING": return 1;
- case "INFO": return 2;
- }
+ switch (t) {
+ case "FATAL":
+ return 0;
+ case "WARNING":
+ return 1;
+ case "INFO":
+ return 2;
+ }
}
function NodeTypeToNumber(t?: NodeType) {
- switch (t) {
- case "github": return 0;
- case "app": return 1;
- case "volume": return 2;
- case "postgresql": return 3;
- case "mongodb": return 4;
- case "gateway-https": return 5;
- case undefined: return 100;
- }
+ switch (t) {
+ case "github":
+ return 0;
+ case "app":
+ return 1;
+ case "volume":
+ return 2;
+ case "postgresql":
+ return 3;
+ case "mongodb":
+ return 4;
+ case "gateway-https":
+ return 5;
+ case undefined:
+ return 100;
+ }
}
function SortingValidator(v: Validator): Validator {
- return (n) => {
- const nt = new Map(n.map((n) => [n.id, NodeTypeToNumber(n.type)]))
- return v(n).sort((a, b) => {
- const at = MessageTypeToNumber(a.type);
- const bt = MessageTypeToNumber(b.type);
- if (a.nodeId === undefined && b.nodeId === undefined) {
- if (at !== bt) {
- return at - bt;
- }
- return a.id.localeCompare(b.id);
- }
- if (a.nodeId === undefined) {
- return -1;
- }
- if (b.nodeId === undefined) {
- return 1;
- }
- if (a.nodeId === b.nodeId) {
- if (at !== bt) {
- return at - bt;
- }
- return a.id.localeCompare(b.id);
- }
- const ant = nt.get(a.id)!;
- const bnt = nt.get(b.id)!;
- if (ant !== bnt) {
- return ant - bnt;
- }
- return a.id.localeCompare(b.id);
- });
- };
+ return (n) => {
+ const nt = new Map(n.map((n) => [n.id, NodeTypeToNumber(n.type)]));
+ return v(n).sort((a, b) => {
+ const at = MessageTypeToNumber(a.type);
+ const bt = MessageTypeToNumber(b.type);
+ if (a.nodeId === undefined && b.nodeId === undefined) {
+ if (at !== bt) {
+ return at - bt;
+ }
+ return a.id.localeCompare(b.id);
+ }
+ if (a.nodeId === undefined) {
+ return -1;
+ }
+ if (b.nodeId === undefined) {
+ return 1;
+ }
+ if (a.nodeId === b.nodeId) {
+ if (at !== bt) {
+ return at - bt;
+ }
+ return a.id.localeCompare(b.id);
+ }
+ const ant = nt.get(a.id)!;
+ const bnt = nt.get(b.id)!;
+ if (ant !== bnt) {
+ return ant - bnt;
+ }
+ return a.id.localeCompare(b.id);
+ });
+ };
}
export function CreateValidators(): Validator {
- return SortingValidator(
- CombineValidators(
- EmptyValidator,
- GitRepositoryValidator,
- ServiceValidator,
- GatewayHTTPSValidator,
- GatewayTCPValidator,
- )
- );
+ return SortingValidator(
+ CombineValidators(
+ EmptyValidator,
+ GitRepositoryValidator,
+ ServiceValidator,
+ GatewayHTTPSValidator,
+ GatewayTCPValidator,
+ ),
+ );
}
function EmptyValidator(nodes: AppNode[]): Message[] {
- nodes = nodes.filter((n) => n.type !== "network");
- if (nodes.length > 0) {
- return [];
- }
- return [{
- id: "no-nodes",
- type: "FATAL",
- message: "Start by importing application source code",
- onHighlight: (store) => store.setHighlightCategory("repository", true),
- onLooseHighlight: (store) => store.setHighlightCategory("repository", false),
- }];
+ nodes = nodes.filter((n) => n.type !== "network");
+ if (nodes.length > 0) {
+ return [];
+ }
+ return [
+ {
+ id: "no-nodes",
+ type: "FATAL",
+ message: "Start by importing application source code",
+ onHighlight: (store) => store.setHighlightCategory("repository", true),
+ onLooseHighlight: (store) => store.setHighlightCategory("repository", false),
+ },
+ ];
}
function GitRepositoryValidator(nodes: AppNode[]): Message[] {
- const git = nodes.filter((n) => n.type === "github");
- const noAddress: Message[] = git.filter((n) => n.data == null || n.data.repository == null).map((n) => ({
- id: `${n.id}-no-address`,
- type: "FATAL",
- nodeId: n.id,
- message: "Configure repository address",
- onHighlight: (store) => store.updateNode(n.id, { selected: true }),
- onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
- } satisfies Message));
- const noApp = git.filter((n) => !nodes.some((i) => i.type === "app" && i.data?.repository?.id === n.id)).map((n) => ({
- id: `${n.id}-no-app`,
- type: "WARNING",
- nodeId: n.id,
- message: "Connect to service",
- onHighlight: (store) => store.setHighlightCategory("Services", true),
- onLooseHighlight: (store) => store.setHighlightCategory("Services", false),
- } satisfies Message));
- return noAddress.concat(noApp);
+ const git = nodes.filter((n) => n.type === "github");
+ const noAddress: Message[] = git
+ .filter((n) => n.data == null || n.data.repository == null)
+ .map(
+ (n) =>
+ ({
+ id: `${n.id}-no-address`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: "Configure repository address",
+ onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+ onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+ }) satisfies Message,
+ );
+ const noApp = git
+ .filter((n) => !nodes.some((i) => i.type === "app" && i.data?.repository?.id === n.id))
+ .map(
+ (n) =>
+ ({
+ id: `${n.id}-no-app`,
+ type: "WARNING",
+ nodeId: n.id,
+ message: "Connect to service",
+ onHighlight: (store) => store.setHighlightCategory("Services", true),
+ onLooseHighlight: (store) => store.setHighlightCategory("Services", false),
+ }) satisfies Message,
+ );
+ return noAddress.concat(noApp);
}
function ServiceValidator(nodes: AppNode[]): Message[] {
- const apps = nodes.filter((n) => n.type === "app");
- const noName = apps.filter((n) => n.data == null || n.data.label == null || n.data.label === "").map((n): Message => ({
- id: `${n.id}-no-name`,
- type: "FATAL",
- nodeId: n.id,
- message: "Name the service",
- onHighlight: (store) => store.updateNode(n.id, { selected: true }),
- onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
- onClick: (store) => {
- store.updateNode(n.id, { selected: true });
- store.updateNodeData<"app">(n.id, {
- activeField: "name",
- });
- },
- }));
- const noSource = apps.filter((n) => n.data == null || n.data.repository == null || n.data.repository.id === "").map((n): Message => ({
- id: `${n.id}-no-repo`,
- type: "FATAL",
- nodeId: n.id,
- message: "Connect to source repository",
- onHighlight: (store) => store.updateNode(n.id, { selected: true }),
- onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
- }));
- const noRuntime = apps.filter((n) => n.data == null || n.data.type == null).map((n): Message => ({
- id: `${n.id}-no-runtime`,
- type: "FATAL",
- nodeId: n.id,
- message: "Choose runtime",
- onHighlight: (store) => store.updateNode(n.id, { selected: true }),
- onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
- onClick: (store) => {
- store.updateNode(n.id, { selected: true });
- store.updateNodeData<"app">(n.id, {
- activeField: "type",
- });
- },
- }));
- const noPorts = apps.filter((n) => n.data == null || n.data.ports == null || n.data.ports.length === 0).map((n): Message => ({
- id: `${n.id}-no-ports`,
- type: "INFO",
- nodeId: n.id,
- message: "Expose ports",
- onHighlight: (store) => store.updateNode(n.id, { selected: true }),
- onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
- }));
- const noIngress = apps.flatMap((n): Message[] => {
- if (n.data == null) {
- return [];
- }
- return (n.data.ports || []).filter((p) => !nodes.filter((i) => i.type === "gateway-https").some((i) => {
- if (i.data && i.data.https && i.data.https.serviceId === n.id && i.data.https.portId === p.id) {
- return true;
- }
- return false;
- })).map((p): Message => ({
- id: `${n.id}-${p.id}-no-ingress`,
- type: "WARNING",
- nodeId: n.id,
- message: `Connect to ingress: ${p.name} - ${p.value}`,
- onHighlight: (store) => {
- store.updateNode(n.id, { selected: true });
- store.setHighlightCategory("gateways", true);
- },
- onLooseHighlight: (store) => {
- store.updateNode(n.id, { selected: false });
- store.setHighlightCategory("gateways", false);
- },
- }));
- });
- const multipleIngress = apps.filter((n) => n.data != null && n.data.ports != null).flatMap((n) => n.data.ports.map((p): Message | undefined => {
- const ing = nodes.filter((i) => i.type === "gateway-https").filter((i) => i.data && i.data.https && i.data.https.serviceId === n.id && i.data.https.portId === p.id);
- if (ing.length < 2) {
- return undefined;
- }
- return {
- id: `${n.id}-${p.id}-multiple-ingress`,
- type: "FATAL",
- 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 }),
- };
- })).filter((m) => m !== undefined);
- return noName.concat(noSource).concat(noRuntime).concat(noPorts).concat(noIngress).concat(multipleIngress);
+ const apps = nodes.filter((n) => n.type === "app");
+ const noName = apps
+ .filter((n) => n.data == null || n.data.label == null || n.data.label === "")
+ .map(
+ (n): Message => ({
+ id: `${n.id}-no-name`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: "Name the service",
+ onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+ onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+ onClick: (store) => {
+ store.updateNode(n.id, { selected: true });
+ store.updateNodeData<"app">(n.id, {
+ activeField: "name",
+ });
+ },
+ }),
+ );
+ const noSource = apps
+ .filter((n) => n.data == null || n.data.repository == null || n.data.repository.id === "")
+ .map(
+ (n): Message => ({
+ id: `${n.id}-no-repo`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: "Connect to source repository",
+ onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+ onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+ }),
+ );
+ const noRuntime = apps
+ .filter((n) => n.data == null || n.data.type == null)
+ .map(
+ (n): Message => ({
+ id: `${n.id}-no-runtime`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: "Choose runtime",
+ onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+ onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+ onClick: (store) => {
+ store.updateNode(n.id, { selected: true });
+ store.updateNodeData<"app">(n.id, {
+ activeField: "type",
+ });
+ },
+ }),
+ );
+ const noPorts = apps
+ .filter((n) => n.data == null || n.data.ports == null || n.data.ports.length === 0)
+ .map(
+ (n): Message => ({
+ id: `${n.id}-no-ports`,
+ type: "INFO",
+ nodeId: n.id,
+ message: "Expose ports",
+ onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+ onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+ }),
+ );
+ const noIngress = apps.flatMap((n): Message[] => {
+ if (n.data == null) {
+ return [];
+ }
+ return (n.data.ports || [])
+ .filter(
+ (p) =>
+ !nodes
+ .filter((i) => i.type === "gateway-https")
+ .some((i) => {
+ if (
+ i.data &&
+ i.data.https &&
+ i.data.https.serviceId === n.id &&
+ i.data.https.portId === p.id
+ ) {
+ return true;
+ }
+ return false;
+ }),
+ )
+ .map(
+ (p): Message => ({
+ id: `${n.id}-${p.id}-no-ingress`,
+ type: "WARNING",
+ nodeId: n.id,
+ message: `Connect to ingress: ${p.name} - ${p.value}`,
+ onHighlight: (store) => {
+ store.updateNode(n.id, { selected: true });
+ store.setHighlightCategory("gateways", true);
+ },
+ onLooseHighlight: (store) => {
+ store.updateNode(n.id, { selected: false });
+ store.setHighlightCategory("gateways", false);
+ },
+ }),
+ );
+ });
+ const multipleIngress = apps
+ .filter((n) => n.data != null && n.data.ports != null)
+ .flatMap((n) =>
+ n.data.ports.map((p): Message | undefined => {
+ const ing = nodes
+ .filter((i) => i.type === "gateway-https")
+ .filter(
+ (i) =>
+ i.data && i.data.https && i.data.https.serviceId === n.id && i.data.https.portId === p.id,
+ );
+ if (ing.length < 2) {
+ return undefined;
+ }
+ return {
+ id: `${n.id}-${p.id}-multiple-ingress`,
+ type: "FATAL",
+ 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 }),
+ };
+ }),
+ )
+ .filter((m) => m !== undefined);
+ return noName.concat(noSource).concat(noRuntime).concat(noPorts).concat(noIngress).concat(multipleIngress);
}
function GatewayHTTPSValidator(nodes: AppNode[]): Message[] {
- const ing = nodes.filter((n) => n.type === "gateway-https");
- const noNetwork: Message[] = ing.filter((n) => n.data == null || n.data.network == null || n.data.network == "" || n.data.subdomain == null || n.data.subdomain == "").map((n): Message => ({
- id: `${n.id}-no-network`,
- type: "FATAL",
- nodeId: n.id,
- message: "Network and subdomain must be defined",
- onHighlight: (store) => store.updateNode(n.id, { selected: true }),
- onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
- }));
- const notConnected: Message[] = ing.filter((n) => n.data == null || n.data.https == null || n.data.https.serviceId == null || n.data.https.serviceId == "" || n.data.https.portId == null || n.data.https.portId == "").map((n) => ({
- id: `${n.id}-not-connected`,
- type: "FATAL",
- nodeId: n.id,
- message: "Connect to a service port",
- onHighlight: (store) => store.updateNode(n.id, { selected: true }),
- onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
- }));
- return noNetwork.concat(notConnected);
+ const ing = nodes.filter((n) => n.type === "gateway-https");
+ const noNetwork: Message[] = ing
+ .filter(
+ (n) =>
+ n.data == null ||
+ n.data.network == null ||
+ n.data.network == "" ||
+ n.data.subdomain == null ||
+ n.data.subdomain == "",
+ )
+ .map(
+ (n): Message => ({
+ id: `${n.id}-no-network`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: "Network and subdomain must be defined",
+ onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+ onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+ }),
+ );
+ const notConnected: Message[] = ing
+ .filter(
+ (n) =>
+ n.data == null ||
+ n.data.https == null ||
+ n.data.https.serviceId == null ||
+ n.data.https.serviceId == "" ||
+ n.data.https.portId == null ||
+ n.data.https.portId == "",
+ )
+ .map((n) => ({
+ id: `${n.id}-not-connected`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: "Connect to a service port",
+ onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+ onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+ }));
+ return noNetwork.concat(notConnected);
}
function GatewayTCPValidator(nodes: AppNode[]): Message[] {
- const ing = nodes.filter((n) => n.type === "gateway-tcp");
- const noNetwork: Message[] = ing.filter((n) => n.data == null || n.data.network == null || n.data.network == "" || n.data.subdomain == null || n.data.subdomain == "").map((n): Message => ({
- id: `${n.id}-no-network`,
- type: "FATAL",
- nodeId: n.id,
- message: "Network and subdomain must be defined",
- onHighlight: (store) => store.updateNode(n.id, { selected: true }),
- onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
- }));
- const notConnected: Message[] = ing.filter((n) => n.data == null || n.data.exposed == null || n.data.exposed.length === 0).map((n) => ({
- id: `${n.id}-not-connected`,
- type: "FATAL",
- nodeId: n.id,
- message: "Connect to a service port",
- onHighlight: (store) => store.updateNode(n.id, { selected: true }),
- onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
- }));
- return noNetwork.concat(notConnected);
+ const ing = nodes.filter((n) => n.type === "gateway-tcp");
+ const noNetwork: Message[] = ing
+ .filter(
+ (n) =>
+ n.data == null ||
+ n.data.network == null ||
+ n.data.network == "" ||
+ n.data.subdomain == null ||
+ n.data.subdomain == "",
+ )
+ .map(
+ (n): Message => ({
+ id: `${n.id}-no-network`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: "Network and subdomain must be defined",
+ onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+ onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+ }),
+ );
+ const notConnected: Message[] = ing
+ .filter((n) => n.data == null || n.data.exposed == null || n.data.exposed.length === 0)
+ .map((n) => ({
+ id: `${n.id}-not-connected`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: "Connect to a service port",
+ onHighlight: (store) => store.updateNode(n.id, { selected: true }),
+ onLooseHighlight: (store) => store.updateNode(n.id, { selected: false }),
+ }));
+ return noNetwork.concat(notConnected);
}
diff --git a/apps/canvas/front/src/lib/github.ts b/apps/canvas/front/src/lib/github.ts
index 8537671..b1732a0 100644
--- a/apps/canvas/front/src/lib/github.ts
+++ b/apps/canvas/front/src/lib/github.ts
@@ -1,36 +1,36 @@
export interface GitHubRepository {
- id: number;
- name: string;
- full_name: string;
- html_url: string;
- ssh_url: string;
- description: string | null;
- private: boolean;
- default_branch: string;
+ id: number;
+ name: string;
+ full_name: string;
+ html_url: string;
+ ssh_url: string;
+ description: string | null;
+ private: boolean;
+ default_branch: string;
}
export interface GitHubService {
- /**
- * Fetches a list of repositories for the authenticated user
- * @returns Promise resolving to an array of GitHub repositories
- */
- getRepositories(): Promise<GitHubRepository[]>;
+ /**
+ * Fetches a list of repositories for the authenticated user
+ * @returns Promise resolving to an array of GitHub repositories
+ */
+ getRepositories(): Promise<GitHubRepository[]>;
}
export class GitHubServiceImpl implements GitHubService {
- private projectId: string;
+ private projectId: string;
- constructor(projectId: string) {
- this.projectId = projectId;
- }
+ constructor(projectId: string) {
+ this.projectId = projectId;
+ }
- async getRepositories(): Promise<GitHubRepository[]> {
- const response = await fetch(`/api/project/${this.projectId}/repos/github`);
+ async getRepositories(): Promise<GitHubRepository[]> {
+ const response = await fetch(`/api/project/${this.projectId}/repos/github`);
- if (!response.ok) {
- throw new Error(`Failed to fetch repositories: ${response.statusText}`);
- }
+ if (!response.ok) {
+ throw new Error(`Failed to fetch repositories: ${response.statusText}`);
+ }
- return response.json();
- }
-}
\ No newline at end of file
+ return response.json();
+ }
+}
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index 2492642..0749fbb 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -1,275 +1,302 @@
-import { v4 as uuidv4 } from "uuid";
-import { create } from 'zustand';
-import { addEdge, applyNodeChanges, applyEdgeChanges, Connection, EdgeChange, useNodes } from '@xyflow/react';
-import type {
- Edge,
- Node,
- OnNodesChange,
- OnEdgesChange,
- OnConnect,
-} from '@xyflow/react';
-import type { DeepPartial } from "react-hook-form";
import { Category, defaultCategories } from "./categories";
import { CreateValidators, Validator } from "./config";
+import { GitHubService, GitHubServiceImpl } from "./github";
+import type { Edge, Node, OnConnect, OnEdgesChange, OnNodesChange } from "@xyflow/react";
+import { addEdge, applyEdgeChanges, applyNodeChanges, Connection, EdgeChange, useNodes } from "@xyflow/react";
+import type { DeepPartial } from "react-hook-form";
+import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
-import { GitHubService, GitHubServiceImpl } from './github';
+import { create } from "zustand";
export type InitData = {
- label: string;
- envVars: BoundEnvVar[];
- ports: Port[];
+ label: string;
+ envVars: BoundEnvVar[];
+ ports: Port[];
};
export type NodeData = InitData & {
- activeField?: string | undefined;
- state: string | null;
+ activeField?: string | undefined;
+ state: string | null;
};
export type PortConnectedTo = {
- serviceId: string;
- portId: string;
-}
+ serviceId: string;
+ portId: string;
+};
export type NetworkData = NodeData & {
- domain: string;
+ domain: string;
};
export type NetworkNode = Node<NetworkData> & {
- type: "network";
+ type: "network";
};
export type GatewayHttpsData = NodeData & {
- network?: string;
- subdomain?: string;
- https?: PortConnectedTo;
- auth?: {
- enabled: boolean;
- groups: string[];
- noAuthPathPatterns: string[];
- }
+ network?: string;
+ subdomain?: string;
+ https?: PortConnectedTo;
+ auth?: {
+ enabled: boolean;
+ groups: string[];
+ noAuthPathPatterns: string[];
+ };
};
export type GatewayHttpsNode = Node<GatewayHttpsData> & {
- type: "gateway-https";
+ type: "gateway-https";
};
export type GatewayTCPData = NodeData & {
- network?: string;
- subdomain?: string;
- exposed: PortConnectedTo[];
- selected?: {
- serviceId?: string;
- portId?: string;
- };
+ network?: string;
+ subdomain?: string;
+ exposed: PortConnectedTo[];
+ selected?: {
+ serviceId?: string;
+ portId?: string;
+ };
};
export type GatewayTCPNode = Node<GatewayTCPData> & {
- type: "gateway-tcp";
+ type: "gateway-tcp";
};
export type Port = {
- id: string;
- name: string;
- value: number;
+ 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",
- "node-23.1.0"
+ "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",
+ "node-23.1.0",
] as const;
-export type ServiceType = typeof ServiceTypes[number];
+export type ServiceType = (typeof ServiceTypes)[number];
export type ServiceData = NodeData & {
- type: ServiceType;
- repository: {
- id: string;
- } | {
- id: string;
- branch: string;
- } | {
- id: string;
- branch: string;
- rootDir: string;
- };
- env: string[];
- volume: string[];
- preBuildCommands: string;
- isChoosingPortToConnect: boolean;
+ type: ServiceType;
+ repository:
+ | {
+ id: string;
+ }
+ | {
+ id: string;
+ branch: string;
+ }
+ | {
+ id: string;
+ branch: string;
+ rootDir: string;
+ };
+ env: string[];
+ volume: string[];
+ preBuildCommands: string;
+ isChoosingPortToConnect: boolean;
};
export type ServiceNode = Node<ServiceData> & {
- type: "app";
+ type: "app";
};
export type VolumeType = "ReadWriteOnce" | "ReadOnlyMany" | "ReadWriteMany" | "ReadWriteOncePod";
export type VolumeData = NodeData & {
- type: VolumeType;
- size: string;
- attachedTo: string[];
+ type: VolumeType;
+ size: string;
+ attachedTo: string[];
};
export type VolumeNode = Node<VolumeData> & {
- type: "volume";
+ type: "volume";
};
export type PostgreSQLData = NodeData & {
- volumeId: string;
+ volumeId: string;
};
export type PostgreSQLNode = Node<PostgreSQLData> & {
- type: "postgresql";
+ type: "postgresql";
};
export type MongoDBData = NodeData & {
- volumeId: string;
+ volumeId: string;
};
export type MongoDBNode = Node<MongoDBData> & {
- type: "mongodb";
+ type: "mongodb";
};
export type GithubData = NodeData & {
- repository?: {
- id: number;
- sshURL: string;
- };
+ repository?: {
+ id: number;
+ sshURL: string;
+ };
};
export type GithubNode = Node<GithubData> & {
- type: "github";
+ type: "github";
};
export type NANode = Node<NodeData> & {
- type: undefined;
+ type: undefined;
};
-export type AppNode = NetworkNode | GatewayHttpsNode | GatewayTCPNode | ServiceNode | VolumeNode | PostgreSQLNode | MongoDBNode | GithubNode | NANode;
+export type AppNode =
+ | NetworkNode
+ | GatewayHttpsNode
+ | GatewayTCPNode
+ | ServiceNode
+ | VolumeNode
+ | PostgreSQLNode
+ | MongoDBNode
+ | GithubNode
+ | NANode;
export function nodeLabel(n: AppNode): string {
- switch (n.type) {
- case "network": return n.data.domain;
- case "app": return n.data.label || "Service";
- case "github": return n.data.repository?.sshURL || "Github";
- case "gateway-https": {
- if (n.data && n.data.network && n.data.subdomain) {
- return `https://${n.data.subdomain}.${n.data.network}`;
- } else {
- return "HTTPS Gateway";
- }
- }
- case "gateway-tcp": {
- if (n.data && n.data.network && n.data.subdomain) {
- return `${n.data.subdomain}.${n.data.network}`;
- } else {
- return "TCP Gateway";
- }
- }
- case "mongodb": return n.data.label || "MongoDB";
- case "postgresql": return n.data.label || "PostgreSQL";
- case "volume": return n.data.label || "Volume";
- case undefined: throw new Error("MUST NOT REACH!");
- }
+ switch (n.type) {
+ case "network":
+ return n.data.domain;
+ case "app":
+ return n.data.label || "Service";
+ case "github":
+ return n.data.repository?.sshURL || "Github";
+ case "gateway-https": {
+ if (n.data && n.data.network && n.data.subdomain) {
+ return `https://${n.data.subdomain}.${n.data.network}`;
+ } else {
+ return "HTTPS Gateway";
+ }
+ }
+ case "gateway-tcp": {
+ if (n.data && n.data.network && n.data.subdomain) {
+ return `${n.data.subdomain}.${n.data.network}`;
+ } else {
+ return "TCP Gateway";
+ }
+ }
+ case "mongodb":
+ return n.data.label || "MongoDB";
+ case "postgresql":
+ return n.data.label || "PostgreSQL";
+ case "volume":
+ return n.data.label || "Volume";
+ case undefined:
+ throw new Error("MUST NOT REACH!");
+ }
}
export function nodeIsConnectable(n: AppNode, handle: string): boolean {
- switch (n.type) {
- case "network":
- return true;
- case "app":
- if (handle === "ports") {
- 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 false;
- }
- return false;
- case "github":
- if (n.data.repository?.id !== undefined) {
- return true;
- }
- return false;
- case "gateway-https":
- if (handle === "subdomain") {
- return n.data.network === undefined;
- }
- return n.data === undefined || n.data.https === undefined;
- case "gateway-tcp":
- if (handle === "subdomain") {
- return n.data.network === undefined;
- }
- return true;
- case "mongodb":
- return true;
- case "postgresql":
- return true;
- case "volume":
- if (n.data === undefined || n.data.type === undefined) {
- return false;
- }
- if (n.data.type === "ReadWriteOnce" || n.data.type === "ReadWriteOncePod") {
- return n.data.attachedTo === undefined || n.data.attachedTo.length === 0;
- }
- return true;
- case undefined: throw new Error("MUST NOT REACH!");
- }
+ switch (n.type) {
+ case "network":
+ return true;
+ case "app":
+ if (handle === "ports") {
+ 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 false;
+ }
+ return false;
+ case "github":
+ if (n.data.repository?.id !== undefined) {
+ return true;
+ }
+ return false;
+ case "gateway-https":
+ if (handle === "subdomain") {
+ return n.data.network === undefined;
+ }
+ return n.data === undefined || n.data.https === undefined;
+ case "gateway-tcp":
+ if (handle === "subdomain") {
+ return n.data.network === undefined;
+ }
+ return true;
+ case "mongodb":
+ return true;
+ case "postgresql":
+ return true;
+ case "volume":
+ if (n.data === undefined || n.data.type === undefined) {
+ return false;
+ }
+ if (n.data.type === "ReadWriteOnce" || n.data.type === "ReadWriteOncePod") {
+ return n.data.attachedTo === undefined || n.data.attachedTo.length === 0;
+ }
+ return true;
+ case undefined:
+ throw new Error("MUST NOT REACH!");
+ }
}
-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 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;
+ name: string;
+ value: string;
};
export function nodeEnvVarNamePort(n: AppNode, portName: string): string {
- return `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS_${portName.toUpperCase()}`;
+ return `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS_${portName.toUpperCase()}`;
}
export function nodeEnvVarNames(n: AppNode): string[] {
- switch (n.type) {
- case "app": return [
- `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS`,
- ...(n.data.ports || []).map((p) => nodeEnvVarNamePort(n, p.name)),
- ];
- case "github": return [];
- case "gateway-https": return [];
- case "gateway-tcp": return [];
- case "mongodb": return [`DODO_MONGODB_${n.data.label.toUpperCase()}_URL`];
- case "postgresql": return [`DODO_POSTGRESQL_${n.data.label.toUpperCase()}_URL`];
- case "volume": return [`DODO_VOLUME_${n.data.label.toUpperCase()}_PATH`];
- case undefined: throw new Error("MUST NOT REACH");
- default: throw new Error("MUST NOT REACH");
- }
+ switch (n.type) {
+ case "app":
+ return [
+ `DODO_SERVICE_${n.data.label.toUpperCase()}_ADDRESS`,
+ ...(n.data.ports || []).map((p) => nodeEnvVarNamePort(n, p.name)),
+ ];
+ case "github":
+ return [];
+ case "gateway-https":
+ return [];
+ case "gateway-tcp":
+ return [];
+ case "mongodb":
+ return [`DODO_MONGODB_${n.data.label.toUpperCase()}_URL`];
+ case "postgresql":
+ return [`DODO_POSTGRESQL_${n.data.label.toUpperCase()}_URL`];
+ case "volume":
+ return [`DODO_VOLUME_${n.data.label.toUpperCase()}_PATH`];
+ case undefined:
+ throw new Error("MUST NOT REACH");
+ default:
+ throw new Error("MUST NOT REACH");
+ }
}
export type NodeType = Exclude<Pick<AppNode, "type">["type"], undefined>;
@@ -277,68 +304,72 @@
export type MessageType = "INFO" | "WARNING" | "FATAL";
export type Message = {
- id: string;
- type: MessageType;
- nodeId?: string;
- message: string;
- onHighlight?: (state: AppState) => void;
- onLooseHighlight?: (state: AppState) => void;
- onClick?: (state: AppState) => void;
+ id: string;
+ type: MessageType;
+ nodeId?: string;
+ message: string;
+ onHighlight?: (state: AppState) => void;
+ onLooseHighlight?: (state: AppState) => void;
+ onClick?: (state: AppState) => void;
};
export const envSchema = z.object({
- deployKey: z.optional(z.string().min(1)),
- networks: z.array(z.object({
- name: z.string().min(1),
- domain: z.string().min(1),
- })).default([]),
- integrations: z.object({
- github: z.boolean(),
- }),
+ deployKey: z.optional(z.string().min(1)),
+ networks: z
+ .array(
+ z.object({
+ name: z.string().min(1),
+ domain: z.string().min(1),
+ }),
+ )
+ .default([]),
+ integrations: z.object({
+ github: z.boolean(),
+ }),
});
export type Env = z.infer<typeof envSchema>;
const defaultEnv: Env = {
- deployKey: undefined,
- networks: [],
- integrations: {
- github: false,
- }
+ deployKey: undefined,
+ networks: [],
+ integrations: {
+ github: false,
+ },
};
export type Project = {
- id: string;
- name: string;
-}
+ id: string;
+ name: string;
+};
export type IntegrationsConfig = {
- github: boolean;
+ github: boolean;
};
type NodeUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>>;
type NodeDataUpdate<T extends NodeType> = DeepPartial<Extract<AppNode, { type: T }>["data"]>;
export type AppState = {
- projectId: string | undefined;
- projects: Project[];
- nodes: AppNode[];
- edges: Edge[];
- categories: Category[];
- messages: Message[];
- env: Env;
- githubService: GitHubService | null;
- setHighlightCategory: (name: string, active: boolean) => void;
- onNodesChange: OnNodesChange<AppNode>;
- onEdgesChange: OnEdgesChange;
- onConnect: OnConnect;
- setNodes: (nodes: AppNode[]) => void;
- setEdges: (edges: Edge[]) => void;
- setProject: (projectId: string | undefined) => void;
- updateNode: <T extends NodeType>(id: string, node: NodeUpdate<T>) => void;
- updateNodeData: <T extends NodeType>(id: string, data: NodeDataUpdate<T>) => void;
- replaceEdge: (c: Connection, id?: string) => void;
- refreshEnv: () => Promise<void>;
+ projectId: string | undefined;
+ projects: Project[];
+ nodes: AppNode[];
+ edges: Edge[];
+ categories: Category[];
+ messages: Message[];
+ env: Env;
+ githubService: GitHubService | null;
+ setHighlightCategory: (name: string, active: boolean) => void;
+ onNodesChange: OnNodesChange<AppNode>;
+ onEdgesChange: OnEdgesChange;
+ onConnect: OnConnect;
+ setNodes: (nodes: AppNode[]) => void;
+ setEdges: (edges: Edge[]) => void;
+ setProject: (projectId: string | undefined) => void;
+ updateNode: <T extends NodeType>(id: string, node: NodeUpdate<T>) => void;
+ updateNodeData: <T extends NodeType>(id: string, data: NodeDataUpdate<T>) => void;
+ replaceEdge: (c: Connection, id?: string) => void;
+ refreshEnv: () => Promise<void>;
};
const projectIdSelector = (state: AppState) => state.projectId;
@@ -348,326 +379,325 @@
const envSelector = (state: AppState) => state.env;
export function useProjectId(): string | undefined {
- return useStateStore(projectIdSelector);
+ return useStateStore(projectIdSelector);
}
export function useCategories(): Category[] {
- return useStateStore(categoriesSelector);
+ return useStateStore(categoriesSelector);
}
export function useMessages(): Message[] {
- return useStateStore(messagesSelector);
+ return useStateStore(messagesSelector);
}
export function useNodeMessages(id: string): Message[] {
- return useMessages().filter((m) => m.nodeId === id);
+ return useMessages().filter((m) => m.nodeId === id);
}
export function useNodeLabel(id: string): string {
- return nodeLabel(useNodes<AppNode>().find((n) => n.id === id)!);
+ return nodeLabel(useNodes<AppNode>().find((n) => n.id === id)!);
}
export function useNodePortName(id: string, portId: string): string {
- return (useNodes<AppNode>().find((n) => n.id === id)!.data.ports || []).find((p) => p.id === portId)!.name;
+ return (useNodes<AppNode>().find((n) => n.id === id)!.data.ports || []).find((p) => p.id === portId)!.name;
}
export function useEnv(): Env {
- return useStateStore(envSelector);
+ return useStateStore(envSelector);
}
export function useGithubService(): GitHubService | null {
- return useStateStore(githubServiceSelector);
+ return useStateStore(githubServiceSelector);
}
const v: Validator = CreateValidators();
export const useStateStore = create<AppState>((set, get): AppState => {
- const setN = (nodes: AppNode[]) => {
- set((state) => ({
- ...state,
- nodes,
- }));
- };
+ const setN = (nodes: AppNode[]) => {
+ set((state) => ({
+ ...state,
+ nodes,
+ }));
+ };
- function updateNodeData<T extends NodeType>(id: string, data: NodeDataUpdate<T>): void {
- setN(
- get().nodes.map((n) => {
- if (n.id === id) {
- return {
- ...n,
- data: {
- ...n.data,
- ...data,
- },
- } as Extract<AppNode, { type: T }>;
- }
- return n;
- })
- );
- }
+ function updateNodeData<T extends NodeType>(id: string, data: NodeDataUpdate<T>): void {
+ setN(
+ get().nodes.map((n) => {
+ if (n.id === id) {
+ return {
+ ...n,
+ data: {
+ ...n.data,
+ ...data,
+ },
+ } as Extract<AppNode, { type: T }>;
+ }
+ return n;
+ }),
+ );
+ }
- function updateNode<T extends NodeType>(id: string, node: NodeUpdate<T>): void {
- setN(
- get().nodes.map((n) => {
- if (n.id === id) {
- return {
- ...n,
- ...node,
- } as Extract<AppNode, { type: T }>;
- }
- return n;
- })
- );
- }
+ function updateNode<T extends NodeType>(id: string, node: NodeUpdate<T>): void {
+ setN(
+ get().nodes.map((n) => {
+ if (n.id === id) {
+ return {
+ ...n,
+ ...node,
+ } as Extract<AppNode, { type: T }>;
+ }
+ return n;
+ }),
+ );
+ }
- function onConnect(c: Connection) {
- const { nodes, edges } = get();
- set({
- edges: addEdge(c, edges),
- });
- const sn = nodes.filter((n) => n.id === c.source)[0]!;
- const tn = nodes.filter((n) => n.id === c.target)[0]!;
- if (tn.type === "network") {
- if (sn.type === "gateway-https") {
- updateNodeData<"gateway-https">(sn.id, {
- network: tn.data.domain,
- });
- } else if (sn.type === "gateway-tcp") {
- updateNodeData<"gateway-tcp">(sn.id, {
- network: tn.data.domain,
- });
- }
- }
- if (tn.type === "app") {
- if (c.sourceHandle === "env_var" && c.targetHandle === "env_var") {
- const sourceEnvVars = nodeEnvVarNames(sn);
- if (sourceEnvVars.length === 0) {
- throw new Error("MUST NOT REACH!");
- }
- const id = uuidv4();
- if (sourceEnvVars.length === 1) {
- updateNode<"app">(c.target, {
- ...tn,
- data: {
- ...tn.data,
- envVars: [
- ...(tn.data.envVars || []),
- {
- id: id,
- source: c.source,
- name: sourceEnvVars[0],
- isEditting: false,
- },
- ],
- },
- });
- } else {
- updateNode<"app">(c.target, {
- ...tn,
- data: {
- ...tn.data,
- envVars: [
- ...(tn.data.envVars || []),
- {
- id: id,
- source: c.source,
- },
- ],
- },
- });
- }
- }
- if (c.sourceHandle === "ports" && c.targetHandle === "env_var") {
- const sourcePorts = sn.data.ports || [];
- const id = uuidv4();
- if (sourcePorts.length === 1) {
- updateNode<"app">(c.target, {
- ...tn,
- data: {
- ...tn.data,
- envVars: [
- ...(tn.data.envVars || []),
- {
- id: id,
- source: c.source,
- name: nodeEnvVarNamePort(sn, sourcePorts[0].name),
- portId: sourcePorts[0].id,
- isEditting: false,
- },
- ],
- },
- });
- }
- }
- }
- if (c.sourceHandle === "volume") {
- updateNodeData<"volume">(c.source, {
- attachedTo: ((sn as VolumeNode).data.attachedTo || []).concat(c.source),
- });
- }
- if (c.targetHandle === "volume") {
- if (tn.type === "postgresql" || tn.type === "mongodb") {
- updateNodeData(c.target, {
- volumeId: c.source,
- });
- }
- }
- if (c.targetHandle === "https") {
- if ((sn.data.ports || []).length === 1) {
- updateNodeData<"gateway-https">(c.target, {
- https: {
- serviceId: c.source,
- portId: sn.data.ports![0].id,
- }
- });
- } else {
- updateNodeData<"gateway-https">(c.target, {
- https: {
- serviceId: c.source,
- portId: "", // TODO(gio)
- }
- });
- }
- }
- if (c.targetHandle === "tcp") {
- const td = tn.data as GatewayTCPData;
- if ((sn.data.ports || []).length === 1) {
- updateNodeData<"gateway-tcp">(c.target, {
- exposed: (td.exposed || []).concat({
- serviceId: c.source,
- portId: sn.data.ports![0].id,
- }),
- });
- } else {
- updateNodeData<"gateway-tcp">(c.target, {
- selected: {
- serviceId: c.source,
- portId: undefined,
- },
- });
- }
- }
- if (sn.type === "app") {
- if (c.sourceHandle === "ports") {
- updateNodeData<"app">(sn.id, {
- isChoosingPortToConnect: true,
- });
- }
- }
- if (tn.type === "app") {
- if (c.targetHandle === "repository") {
- updateNodeData<"app">(tn.id, {
- repository: {
- id: c.source,
- branch: "master",
- rootDir: "/",
- }
- });
- }
- }
- }
- return {
- projectId: undefined,
- projects: [],
- nodes: [],
- edges: [],
- categories: defaultCategories,
- messages: v([]),
- env: defaultEnv,
- githubService: null,
- setHighlightCategory: (name, active) => {
- set({
- categories: get().categories.map(
- (c) => {
- if (c.title.toLowerCase() !== name.toLowerCase()) {
- return c;
- } else {
- return {
- ...c,
- active,
- }
- }
- })
- });
- },
- onNodesChange: (changes) => {
- const nodes = applyNodeChanges(changes, get().nodes);
- setN(nodes);
- },
- onEdgesChange: (changes) => {
- set({
- edges: applyEdgeChanges(changes, get().edges),
- });
- },
- setNodes: (nodes) => {
- setN(nodes);
- },
- setEdges: (edges) => {
- set({ edges });
- },
- replaceEdge: (c, id) => {
- let change: EdgeChange;
- if (id === undefined) {
- change = {
- type: "add",
- item: {
- id: uuidv4(),
- ...c,
- }
- };
- onConnect(c);
- } else {
- change = {
- type: "replace",
- id,
- item: {
- id,
- ...c,
- }
- };
- }
- set({
- edges: applyEdgeChanges([change], get().edges),
- })
- },
- updateNode,
- updateNodeData,
- onConnect,
- refreshEnv: async () => {
- const projectId = get().projectId;
- let env: Env = defaultEnv;
+ function onConnect(c: Connection) {
+ const { nodes, edges } = get();
+ set({
+ edges: addEdge(c, edges),
+ });
+ const sn = nodes.filter((n) => n.id === c.source)[0]!;
+ const tn = nodes.filter((n) => n.id === c.target)[0]!;
+ if (tn.type === "network") {
+ if (sn.type === "gateway-https") {
+ updateNodeData<"gateway-https">(sn.id, {
+ network: tn.data.domain,
+ });
+ } else if (sn.type === "gateway-tcp") {
+ updateNodeData<"gateway-tcp">(sn.id, {
+ network: tn.data.domain,
+ });
+ }
+ }
+ if (tn.type === "app") {
+ if (c.sourceHandle === "env_var" && c.targetHandle === "env_var") {
+ const sourceEnvVars = nodeEnvVarNames(sn);
+ if (sourceEnvVars.length === 0) {
+ throw new Error("MUST NOT REACH!");
+ }
+ const id = uuidv4();
+ if (sourceEnvVars.length === 1) {
+ updateNode<"app">(c.target, {
+ ...tn,
+ data: {
+ ...tn.data,
+ envVars: [
+ ...(tn.data.envVars || []),
+ {
+ id: id,
+ source: c.source,
+ name: sourceEnvVars[0],
+ isEditting: false,
+ },
+ ],
+ },
+ });
+ } else {
+ updateNode<"app">(c.target, {
+ ...tn,
+ data: {
+ ...tn.data,
+ envVars: [
+ ...(tn.data.envVars || []),
+ {
+ id: id,
+ source: c.source,
+ },
+ ],
+ },
+ });
+ }
+ }
+ if (c.sourceHandle === "ports" && c.targetHandle === "env_var") {
+ const sourcePorts = sn.data.ports || [];
+ const id = uuidv4();
+ if (sourcePorts.length === 1) {
+ updateNode<"app">(c.target, {
+ ...tn,
+ data: {
+ ...tn.data,
+ envVars: [
+ ...(tn.data.envVars || []),
+ {
+ id: id,
+ source: c.source,
+ name: nodeEnvVarNamePort(sn, sourcePorts[0].name),
+ portId: sourcePorts[0].id,
+ isEditting: false,
+ },
+ ],
+ },
+ });
+ }
+ }
+ }
+ if (c.sourceHandle === "volume") {
+ updateNodeData<"volume">(c.source, {
+ attachedTo: ((sn as VolumeNode).data.attachedTo || []).concat(c.source),
+ });
+ }
+ if (c.targetHandle === "volume") {
+ if (tn.type === "postgresql" || tn.type === "mongodb") {
+ updateNodeData(c.target, {
+ volumeId: c.source,
+ });
+ }
+ }
+ if (c.targetHandle === "https") {
+ if ((sn.data.ports || []).length === 1) {
+ updateNodeData<"gateway-https">(c.target, {
+ https: {
+ serviceId: c.source,
+ portId: sn.data.ports![0].id,
+ },
+ });
+ } else {
+ updateNodeData<"gateway-https">(c.target, {
+ https: {
+ serviceId: c.source,
+ portId: "", // TODO(gio)
+ },
+ });
+ }
+ }
+ if (c.targetHandle === "tcp") {
+ const td = tn.data as GatewayTCPData;
+ if ((sn.data.ports || []).length === 1) {
+ updateNodeData<"gateway-tcp">(c.target, {
+ exposed: (td.exposed || []).concat({
+ serviceId: c.source,
+ portId: sn.data.ports![0].id,
+ }),
+ });
+ } else {
+ updateNodeData<"gateway-tcp">(c.target, {
+ selected: {
+ serviceId: c.source,
+ portId: undefined,
+ },
+ });
+ }
+ }
+ if (sn.type === "app") {
+ if (c.sourceHandle === "ports") {
+ updateNodeData<"app">(sn.id, {
+ isChoosingPortToConnect: true,
+ });
+ }
+ }
+ if (tn.type === "app") {
+ if (c.targetHandle === "repository") {
+ updateNodeData<"app">(tn.id, {
+ repository: {
+ id: c.source,
+ branch: "master",
+ rootDir: "/",
+ },
+ });
+ }
+ }
+ }
+ return {
+ projectId: undefined,
+ projects: [],
+ nodes: [],
+ edges: [],
+ categories: defaultCategories,
+ messages: v([]),
+ env: defaultEnv,
+ githubService: null,
+ setHighlightCategory: (name, active) => {
+ set({
+ categories: get().categories.map((c) => {
+ if (c.title.toLowerCase() !== name.toLowerCase()) {
+ return c;
+ } else {
+ return {
+ ...c,
+ active,
+ };
+ }
+ }),
+ });
+ },
+ onNodesChange: (changes) => {
+ const nodes = applyNodeChanges(changes, get().nodes);
+ setN(nodes);
+ },
+ onEdgesChange: (changes) => {
+ set({
+ edges: applyEdgeChanges(changes, get().edges),
+ });
+ },
+ setNodes: (nodes) => {
+ setN(nodes);
+ },
+ setEdges: (edges) => {
+ set({ edges });
+ },
+ replaceEdge: (c, id) => {
+ let change: EdgeChange;
+ if (id === undefined) {
+ change = {
+ type: "add",
+ item: {
+ id: uuidv4(),
+ ...c,
+ },
+ };
+ onConnect(c);
+ } else {
+ change = {
+ type: "replace",
+ id,
+ item: {
+ id,
+ ...c,
+ },
+ };
+ }
+ set({
+ edges: applyEdgeChanges([change], get().edges),
+ });
+ },
+ updateNode,
+ updateNodeData,
+ onConnect,
+ refreshEnv: async () => {
+ const projectId = get().projectId;
+ let env: Env = defaultEnv;
- try {
- if (projectId) {
- const response = await fetch(`/api/project/${projectId}/env`);
- if (response.ok) {
- const data = await response.json();
- const result = envSchema.safeParse(data);
- if (result.success) {
- env = result.data;
- } else {
- console.error("Invalid env data:", result.error);
- }
- }
- }
- } catch (error) {
- console.error("Failed to fetch integrations:", error);
- } finally {
- set({ env: env });
- if (env.integrations.github) {
- set({ githubService: new GitHubServiceImpl(projectId!) });
- } else {
- set({ githubService: null });
- }
- }
- },
- setProject: (projectId) => {
- set({
- projectId,
- });
- if (projectId) {
- get().refreshEnv();
- }
- },
- };
+ try {
+ if (projectId) {
+ const response = await fetch(`/api/project/${projectId}/env`);
+ if (response.ok) {
+ const data = await response.json();
+ const result = envSchema.safeParse(data);
+ if (result.success) {
+ env = result.data;
+ } else {
+ console.error("Invalid env data:", result.error);
+ }
+ }
+ }
+ } catch (error) {
+ console.error("Failed to fetch integrations:", error);
+ } finally {
+ set({ env: env });
+ if (env.integrations.github) {
+ set({ githubService: new GitHubServiceImpl(projectId!) });
+ } else {
+ set({ githubService: null });
+ }
+ }
+ },
+ setProject: (projectId) => {
+ set({
+ projectId,
+ });
+ if (projectId) {
+ get().refreshEnv();
+ }
+ },
+ };
});
diff --git a/apps/canvas/front/src/lib/utils.ts b/apps/canvas/front/src/lib/utils.ts
index bd0c391..ac680b3 100644
--- a/apps/canvas/front/src/lib/utils.ts
+++ b/apps/canvas/front/src/lib/utils.ts
@@ -1,6 +1,6 @@
-import { clsx, type ClassValue } from "clsx"
-import { twMerge } from "tailwind-merge"
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs))
+ return twMerge(clsx(inputs));
}