Cavnas: Implement basic service discovery logic
Change-Id: I71b25076dba94d6491ad4db748b259870991c526
diff --git a/apps/canvas/front/src/ProjectSelect.tsx b/apps/canvas/front/src/ProjectSelect.tsx
index 278db67..312e6aa 100644
--- a/apps/canvas/front/src/ProjectSelect.tsx
+++ b/apps/canvas/front/src/ProjectSelect.tsx
@@ -117,7 +117,6 @@
});
});
}, [name, setCreateNewOpen, toast, refreshProjects]);
- console.log("asd", projectId);
return (
<>
<Select onValueChange={onSelect} value={projectId}>
diff --git a/apps/canvas/front/src/Tools.tsx b/apps/canvas/front/src/Tools.tsx
index f355d3e..4c9b19e 100644
--- a/apps/canvas/front/src/Tools.tsx
+++ b/apps/canvas/front/src/Tools.tsx
@@ -27,7 +27,7 @@
<TabsContent value="gateways">
<Gateways />
</TabsContent>
- <TabsContent value="deployKeys">{env.deployKey && <>{env.deployKey}</>}</TabsContent>
+ <TabsContent value="deployKeys">{env.deployKeyPublic && <>{env.deployKeyPublic}</>}</TabsContent>
</div>
</Tabs>
);
diff --git a/apps/canvas/front/src/components/actions.tsx b/apps/canvas/front/src/components/actions.tsx
index 24198cb..dbb5ea6 100644
--- a/apps/canvas/front/src/components/actions.tsx
+++ b/apps/canvas/front/src/components/actions.tsx
@@ -169,6 +169,10 @@
}
const resp = await fetch(`/api/project/${projectId}`, {
method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ state: JSON.stringify(instance.toObject()) }),
});
if (resp.ok) {
clear();
@@ -177,7 +181,7 @@
} else {
error("Failed to delete project", await resp.text());
}
- }, [store, clear, projectId, info, error]);
+ }, [store, clear, projectId, info, error, instance]);
const reload = useCallback(async () => {
if (projectId == null) {
return;
diff --git a/apps/canvas/front/src/components/node-github.tsx b/apps/canvas/front/src/components/node-github.tsx
index 4c065be..dd7c68a 100644
--- a/apps/canvas/front/src/components/node-github.tsx
+++ b/apps/canvas/front/src/components/node-github.tsx
@@ -1,6 +1,19 @@
import { NodeRect } from "./node-rect";
-import { GithubNode, nodeIsConnectable, nodeLabel, useStateStore, useGithubService } from "@/lib/state";
-import { useEffect, useMemo, useState } from "react";
+import {
+ GithubNode,
+ nodeIsConnectable,
+ nodeLabel,
+ serviceAnalyzisSchema,
+ useStateStore,
+ useGithubService,
+ ServiceType,
+ ServiceData,
+ useGithubRepositories,
+ useGithubRepositoriesLoading,
+ useGithubRepositoriesError,
+ useFetchGithubRepositories,
+} from "@/lib/state";
+import { useCallback, useEffect, useMemo, useState } from "react";
import { z } from "zod";
import { DeepPartial, EventType, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -10,7 +23,12 @@
import { GitHubRepository } from "../lib/github";
import { useProjectId } from "@/lib/state";
import { Alert, AlertDescription } from "./ui/alert";
-import { AlertCircle } from "lucide-react";
+import { AlertCircle, LoaderCircle, RefreshCw } from "lucide-react";
+import { Button } from "./ui/button";
+import { v4 as uuidv4 } from "uuid";
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
+import { Switch } from "./ui/switch";
+import { Label } from "./ui/label";
export function NodeGithub(node: GithubNode) {
const { id, selected } = node;
@@ -39,34 +57,42 @@
export function NodeGithubDetails({ id, data, disabled }: GithubNode & { disabled?: boolean }) {
const store = useStateStore();
const projectId = useProjectId();
- const [repos, setRepos] = useState<GitHubRepository[]>([]);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<string | null>(null);
const githubService = useGithubService();
+ const storeRepos = useGithubRepositories();
+ const isLoadingRepos = useGithubRepositoriesLoading();
+ const repoError = useGithubRepositoriesError();
+ const fetchStoreRepositories = useFetchGithubRepositories();
+
+ const [displayRepos, setDisplayRepos] = useState<GitHubRepository[]>([]);
+
+ const [isAnalyzing, setIsAnalyzing] = useState(false);
+ const [showModal, setShowModal] = useState(false);
+ const [discoveredServices, setDiscoveredServices] = useState<z.infer<typeof serviceAnalyzisSchema>[]>([]);
+ const [selectedServices, setSelectedServices] = useState<Record<string, boolean>>({});
+
useEffect(() => {
+ let currentRepoInStore = false;
if (data.repository) {
- const { id, sshURL } = data.repository;
- setRepos((prevRepos) => {
- if (!prevRepos.some((r) => r.id === id)) {
- return [
- ...prevRepos,
- {
- id,
- name: sshURL.split("/").pop() || "",
- full_name: sshURL.split("/").slice(-2).join("/"),
- html_url: "",
- ssh_url: sshURL,
- description: null,
- private: false,
- default_branch: "main",
- },
- ];
- }
- return prevRepos;
- });
+ currentRepoInStore = storeRepos.some((r) => r.id === data.repository!.id);
}
- }, [data.repository]);
+
+ if (data.repository && !currentRepoInStore) {
+ const currentRepoForDisplay: GitHubRepository = {
+ id: data.repository.id,
+ name: data.repository.sshURL.split("/").pop() || "",
+ full_name: data.repository.fullName || data.repository.sshURL.split("/").slice(-2).join("/"),
+ html_url: "",
+ ssh_url: data.repository.sshURL,
+ description: null,
+ private: false,
+ default_branch: "main",
+ };
+ setDisplayRepos([currentRepoForDisplay, ...storeRepos.filter((r) => r.id !== data.repository!.id)]);
+ } else {
+ setDisplayRepos(storeRepos);
+ }
+ }, [data.repository, storeRepos]);
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
@@ -77,6 +103,10 @@
});
useEffect(() => {
+ form.reset({ repositoryId: data.repository?.id });
+ }, [data.repository?.id, form]);
+
+ useEffect(() => {
const sub = form.watch(
(
value: DeepPartial<z.infer<typeof schema>>,
@@ -88,7 +118,7 @@
switch (name) {
case "repositoryId":
if (value.repositoryId) {
- const repo = repos.find((r) => r.id === value.repositoryId);
+ const repo = displayRepos.find((r) => r.id === value.repositoryId);
if (repo) {
store.updateNodeData<"github">(id, {
repository: {
@@ -104,26 +134,87 @@
},
);
return () => sub.unsubscribe();
- }, [form, store, id, repos]);
+ }, [form, store, id, displayRepos]);
- useEffect(() => {
- const fetchRepositories = async () => {
- if (!githubService) return;
+ const analyze = useCallback(async () => {
+ if (!data.repository?.sshURL) return;
- setLoading(true);
- setError(null);
- try {
- const repositories = await githubService.getRepositories();
- setRepos(repositories);
- } catch (err) {
- setError(err instanceof Error ? err.message : "Failed to fetch repositories");
- } finally {
- setLoading(false);
+ setIsAnalyzing(true);
+ try {
+ const resp = await fetch(`/api/project/${projectId}/analyze`, {
+ method: "POST",
+ body: JSON.stringify({
+ address: data.repository?.sshURL,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const servicesResult = z.array(serviceAnalyzisSchema).safeParse(await resp.json());
+ if (!servicesResult.success) {
+ console.error(servicesResult.error);
+ setIsAnalyzing(false);
+ return;
}
- };
- fetchRepositories();
- }, [githubService]);
+ setDiscoveredServices(servicesResult.data);
+ const initialSelectedServices: Record<string, boolean> = {};
+ servicesResult.data.forEach((service) => {
+ initialSelectedServices[service.name] = true;
+ });
+ setSelectedServices(initialSelectedServices);
+ setShowModal(true);
+ } catch (err) {
+ console.error("Analysis failed:", err);
+ } finally {
+ setIsAnalyzing(false);
+ }
+ }, [projectId, data.repository?.sshURL]);
+
+ const handleImportServices = () => {
+ discoveredServices.forEach((service) => {
+ if (selectedServices[service.name]) {
+ const newNodeData: Omit<ServiceData, "activeField" | "state"> = {
+ label: service.name,
+ repository: {
+ id: id,
+ },
+ info: service,
+ type: "nodejs:24.0.2" as ServiceType,
+ env: [],
+ volume: [],
+ preBuildCommands: "",
+ isChoosingPortToConnect: false,
+ envVars: [],
+ ports: [],
+ };
+ const newNodeId = uuidv4();
+ store.addNode({
+ id: newNodeId,
+ type: "app",
+ data: newNodeData,
+ });
+ let edges = store.edges;
+ edges = edges.concat({
+ id: uuidv4(),
+ source: id,
+ sourceHandle: "repository",
+ target: newNodeId,
+ targetHandle: "repository",
+ });
+ store.setEdges(edges);
+ }
+ });
+ setShowModal(false);
+ setDiscoveredServices([]);
+ setSelectedServices({});
+ };
+
+ const handleCancelModal = () => {
+ setShowModal(false);
+ setDiscoveredServices([]);
+ setSelectedServices({});
+ };
return (
<>
@@ -134,32 +225,57 @@
name="repositoryId"
render={({ field }) => (
<FormItem>
- <Select
- onValueChange={(value) => field.onChange(Number(value))}
- value={field.value?.toString()}
- disabled={loading || !projectId || !githubService || disabled}
- >
- <FormControl>
- <SelectTrigger>
- <SelectValue
- placeholder={
- githubService ? "Select a repository" : "GitHub not configured"
- }
- />
- </SelectTrigger>
- </FormControl>
- <SelectContent>
- {repos.map((repo) => (
- <SelectItem key={repo.id} value={repo.id.toString()}>
- {repo.full_name}
- {repo.description && ` - ${repo.description}`}
- </SelectItem>
- ))}
- </SelectContent>
- </Select>
+ <div className="flex items-center gap-2 w-full">
+ <div className="flex-grow">
+ <Select
+ onValueChange={(value) => field.onChange(Number(value))}
+ value={field.value?.toString()}
+ disabled={isLoadingRepos || !projectId || !githubService || disabled}
+ >
+ <FormControl>
+ <SelectTrigger>
+ <SelectValue
+ placeholder={
+ githubService
+ ? isLoadingRepos
+ ? "Loading..."
+ : displayRepos.length === 0
+ ? "No repositories found"
+ : "Select a repository"
+ : "GitHub not configured"
+ }
+ />
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {displayRepos.map((repo) => (
+ <SelectItem key={repo.id} value={repo.id.toString()}>
+ {repo.full_name}
+ {repo.description && ` - ${repo.description}`}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ </div>
+ {isLoadingRepos && (
+ <Button variant="ghost" size="icon" disabled>
+ <LoaderCircle className="h-5 w-5 animate-spin text-muted-foreground" />
+ </Button>
+ )}
+ {!isLoadingRepos && githubService && (
+ <Button
+ variant="ghost"
+ size="icon"
+ onClick={fetchStoreRepositories}
+ disabled={disabled}
+ aria-label="Refresh repositories"
+ >
+ <RefreshCw className="h-5 w-5 text-muted-foreground" />
+ </Button>
+ )}
+ </div>
<FormMessage />
- {error && <p className="text-sm text-red-500">{error}</p>}
- {loading && <p className="text-sm text-gray-500">Loading repositories...</p>}
+ {repoError && <p className="text-sm text-red-500 mt-1">{repoError}</p>}
{!githubService && (
<Alert variant="destructive" className="mt-2">
<AlertCircle className="h-4 w-4" />
@@ -173,6 +289,62 @@
/>
</form>
</Form>
+ <Button disabled={!data.repository?.sshURL || isAnalyzing || !githubService || disabled} onClick={analyze}>
+ {isAnalyzing && <LoaderCircle className="mr-2 h-4 w-4 animate-spin" />}
+ Scan for services
+ </Button>
+ {showModal && (
+ <Dialog open={showModal} onOpenChange={setShowModal}>
+ <DialogContent className="sm:max-w-[425px]">
+ <DialogHeader>
+ <DialogTitle>Discovered Services</DialogTitle>
+ <DialogDescription>Select the services you want to import.</DialogDescription>
+ </DialogHeader>
+ <div className="grid gap-4 py-4">
+ {discoveredServices.map((service) => (
+ <div key={service.name} className="flex flex-col space-y-2 p-2 border rounded-md">
+ <div className="flex items-center space-x-2">
+ <Switch
+ id={service.name}
+ checked={selectedServices[service.name]}
+ onCheckedChange={(checked: boolean) =>
+ setSelectedServices((prev) => ({
+ ...prev,
+ [service.name]: checked,
+ }))
+ }
+ />
+ <Label htmlFor={service.name} className="font-semibold">
+ {service.name}
+ </Label>
+ </div>
+ <div className="pl-6 text-sm text-gray-600">
+ <p>
+ <span className="font-medium">Location:</span> {service.location}
+ </p>
+ {service.configVars && service.configVars.length > 0 && (
+ <div className="mt-1">
+ <p className="font-medium">Environment Variables:</p>
+ <ul className="list-disc list-inside pl-4">
+ {service.configVars.map((envVar) => (
+ <li key={envVar.name}>{envVar.name}</li>
+ ))}
+ </ul>
+ </div>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ <DialogFooter>
+ <Button variant="outline" onClick={handleCancelModal}>
+ Cancel
+ </Button>
+ <Button onClick={handleImportServices}>Import</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )}
</>
);
}
diff --git a/apps/canvas/front/src/components/ui/switch.tsx b/apps/canvas/front/src/components/ui/switch.tsx
new file mode 100644
index 0000000..5139bf2
--- /dev/null
+++ b/apps/canvas/front/src/components/ui/switch.tsx
@@ -0,0 +1,29 @@
+"use client";
+
+import * as React from "react";
+import * as SwitchPrimitives from "@radix-ui/react-switch";
+
+import { cn } from "@/lib/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 };
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index 572fc78..513f1f0 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -309,6 +309,7 @@
EmptyValidator,
GitRepositoryValidator,
ServiceValidator,
+ ServiceAnalyzisValidator,
GatewayHTTPSValidator,
GatewayTCPValidator,
),
@@ -488,6 +489,56 @@
return noName.concat(noSource).concat(noRuntime).concat(noPorts).concat(noIngress).concat(multipleIngress);
}
+function ServiceAnalyzisValidator(nodes: AppNode[]): Message[] {
+ const apps = nodes.filter((n) => n.type === "app");
+ return apps
+ .filter((n) => n.data.info != null)
+ .flatMap((n) => {
+ return n.data
+ .info!.configVars.map((cv): Message | undefined => {
+ if (cv.semanticType === "PORT") {
+ if (
+ !(n.data.envVars || []).some(
+ (p) => ("name" in p && p.name === cv.name) || ("alias" in p && p.alias === cv.name),
+ )
+ ) {
+ return {
+ id: `${n.id}-missing-port-${cv.name}`,
+ type: "WARNING",
+ nodeId: n.id,
+ message: `Service requires port env variable ${cv.name}`,
+ };
+ }
+ }
+ if (cv.category === "EnvironmentVariable") {
+ if (
+ !(n.data.envVars || []).some(
+ (p) => ("name" in p && p.name === cv.name) || ("alias" in p && p.alias === cv.name),
+ )
+ ) {
+ if (cv.semanticType === "FILESYSTEM_PATH") {
+ return {
+ id: `${n.id}-missing-env-${cv.name}`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: `Service requires env variable ${cv.name}, representing filesystem path`,
+ };
+ } else if (cv.semanticType === "POSTGRES_URL") {
+ return {
+ id: `${n.id}-missing-env-${cv.name}`,
+ type: "FATAL",
+ nodeId: n.id,
+ message: `Service requires env variable ${cv.name}, representing postgres connection URL`,
+ };
+ }
+ }
+ }
+ return undefined;
+ })
+ .filter((m) => m !== undefined);
+ });
+}
+
function GatewayHTTPSValidator(nodes: AppNode[]): Message[] {
const ing = nodes.filter((n) => n.type === "gateway-https");
const noNetwork: Message[] = ing
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index b7413d9..5d8013d 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -1,6 +1,6 @@
import { Category, defaultCategories } from "./categories";
import { CreateValidators, Validator } from "./config";
-import { GitHubService, GitHubServiceImpl } from "./github";
+import { GitHubService, GitHubServiceImpl, GitHubRepository } from "./github";
import type { Edge, Node, OnConnect, OnEdgesChange, OnNodesChange, Viewport as ReactFlowViewport } from "@xyflow/react";
import {
addEdge,
@@ -16,6 +16,41 @@
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[];
@@ -125,6 +160,7 @@
codeServerNodeId: string;
sshNodeId: string;
};
+ info?: z.infer<typeof serviceAnalyzisSchema>;
};
export type ServiceNode = Node<ServiceData> & {
@@ -425,7 +461,8 @@
export const envSchema = z.object({
managerAddr: z.optional(z.string().min(1)),
- deployKey: z.optional(z.nullable(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({
@@ -451,7 +488,8 @@
const defaultEnv: Env = {
managerAddr: undefined,
- deployKey: undefined,
+ deployKeyPublic: undefined,
+ instanceId: undefined,
networks: [],
integrations: {
github: false,
@@ -499,6 +537,9 @@
viewport: Viewport;
setViewport: (viewport: Viewport) => void;
githubService: GitHubService | null;
+ githubRepositories: GitHubRepository[];
+ githubRepositoriesLoading: boolean;
+ githubRepositoriesError: string | null;
setHighlightCategory: (name: string, active: boolean) => void;
onNodesChange: OnNodesChange<AppNode>;
onEdgesChange: OnEdgesChange;
@@ -512,6 +553,7 @@
updateNodeData: <T extends NodeType>(id: string, data: NodeDataUpdate<T>) => void;
replaceEdge: (c: Connection, id?: string) => void;
refreshEnv: () => Promise<void>;
+ fetchGithubRepositories: () => Promise<void>;
};
const projectIdSelector = (state: AppState) => state.projectId;
@@ -520,6 +562,9 @@
const githubServiceSelector = (state: AppState) => state.githubService;
const envSelector = (state: AppState) => state.env;
const zoomSelector = (state: AppState) => state.zoom;
+const githubRepositoriesSelector = (state: AppState) => state.githubRepositories;
+const githubRepositoriesLoadingSelector = (state: AppState) => state.githubRepositoriesLoading;
+const githubRepositoriesErrorSelector = (state: AppState) => state.githubRepositoriesError;
export function useZoom(): ReactFlowViewport {
return useStateStore(zoomSelector);
@@ -561,6 +606,22 @@
return useStateStore(githubServiceSelector);
}
+export function useGithubRepositories(): GitHubRepository[] {
+ return useStateStore(githubRepositoriesSelector);
+}
+
+export function useGithubRepositoriesLoading(): boolean {
+ return useStateStore(githubRepositoriesLoadingSelector);
+}
+
+export function useGithubRepositoriesError(): string | null {
+ return useStateStore(githubRepositoriesErrorSelector);
+}
+
+export function useFetchGithubRepositories(): () => Promise<void> {
+ return useStateStore((state) => state.fetchGithubRepositories);
+}
+
export function useMode(): "edit" | "deploy" {
return useStateStore((state) => state.mode);
}
@@ -851,6 +912,29 @@
}
}
}
+
+ const fetchGithubRepositories = async () => {
+ const { githubService, projectId } = get();
+ if (!githubService || !projectId) {
+ set({
+ githubRepositories: [],
+ githubRepositoriesError: "GitHub service or Project ID not available.",
+ githubRepositoriesLoading: false,
+ });
+ return;
+ }
+
+ set({ githubRepositoriesLoading: true, githubRepositoriesError: null });
+ try {
+ const repos = await githubService.getRepositories();
+ set({ githubRepositories: repos, githubRepositoriesLoading: false });
+ } catch (error) {
+ console.error("Failed to fetch GitHub repositories in store:", error);
+ const errorMessage = error instanceof Error ? error.message : "Unknown error fetching repositories";
+ set({ githubRepositories: [], githubRepositoriesError: errorMessage, githubRepositoriesLoading: false });
+ }
+ };
+
return {
projectId: undefined,
mode: "edit",
@@ -873,6 +957,9 @@
zoom: 1,
},
githubService: null,
+ githubRepositories: [],
+ githubRepositoriesLoading: false,
+ githubRepositoriesError: null,
setViewport: (viewport) => {
const { viewport: vp } = get();
if (
@@ -970,13 +1057,30 @@
} catch (error) {
console.error("Failed to fetch integrations:", error);
} finally {
- if (JSON.stringify(get().env) !== JSON.stringify(env)) {
+ const oldEnv = get().env;
+ const oldGithubIntegrationStatus = oldEnv.integrations.github;
+ if (JSON.stringify(oldEnv) !== JSON.stringify(env)) {
set({ env });
injectNetworkNodes();
+ let ghService = null;
if (env.integrations.github) {
- set({ githubService: new GitHubServiceImpl(projectId!) });
- } else {
- set({ githubService: null });
+ ghService = new GitHubServiceImpl(projectId!);
+ }
+ if (get().githubService !== ghService || (ghService && !get().githubService)) {
+ set({ githubService: ghService });
+ }
+ if (
+ ghService &&
+ (oldGithubIntegrationStatus !== env.integrations.github || !oldEnv.integrations.github)
+ ) {
+ get().fetchGithubRepositories();
+ }
+ if (!env.integrations.github) {
+ set({
+ githubRepositories: [],
+ githubRepositoriesError: null,
+ githubRepositoriesLoading: false,
+ });
}
}
}
@@ -992,10 +1096,13 @@
stopRefreshEnvInterval();
set({
projectId,
+ githubRepositories: [],
+ githubRepositoriesLoading: false,
+ githubRepositoriesError: null,
});
if (projectId) {
await get().refreshEnv();
- if (get().env.deployKey) {
+ if (get().env.instanceId) {
set({ mode: "deploy" });
} else {
set({ mode: "edit" });
@@ -1008,8 +1115,12 @@
edges: [],
env: defaultEnv,
githubService: null,
+ githubRepositories: [],
+ githubRepositoriesLoading: false,
+ githubRepositoriesError: null,
});
}
},
+ fetchGithubRepositories: fetchGithubRepositories,
};
});