Canvas: Add Reload button on Monitoring page
Change-Id: I593d9068870bcd5f0d43680af4a08d814a18a2a9
diff --git a/apps/canvas/back/src/index.ts b/apps/canvas/back/src/index.ts
index de18582..cfd8955 100644
--- a/apps/canvas/back/src/index.ts
+++ b/apps/canvas/back/src/index.ts
@@ -662,6 +662,27 @@
}
};
+const handleReloadWorker: express.Handler = async (req, resp) => {
+ const projectId = Number(req.params["projectId"]);
+ const serviceName = req.params["serviceName"];
+ const workerId = req.params["workerId"];
+
+ const projectMonitor = projectMonitors.get(projectId);
+ if (!projectMonitor) {
+ resp.status(404).send({ error: "Project monitor not found" });
+ return;
+ }
+
+ try {
+ await projectMonitor.reloadWorker(serviceName, workerId);
+ resp.status(200).send({ message: "Worker reload initiated" });
+ } catch (error) {
+ console.error(`Failed to reload worker ${workerId} in service ${serviceName} for project ${projectId}:`, error);
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
+ resp.status(500).send({ error: `Failed to reload worker: ${errorMessage}` });
+ }
+};
+
const auth = (req: express.Request, resp: express.Response, next: express.NextFunction) => {
const userId = req.get("x-forwarded-userid");
const username = req.get("x-forwarded-user");
@@ -750,11 +771,13 @@
projectRouter.get("/:projectId/repos/github", handleGithubRepos);
projectRouter.post("/:projectId/github-token", handleUpdateGithubToken);
projectRouter.get("/:projectId/env", handleEnv);
+ projectRouter.post("/:projectId/reload/:serviceName/:workerId", handleReloadWorker);
projectRouter.post("/:projectId/reload", handleReload);
projectRouter.get("/:projectId/logs/:service/:workerId", handleServiceLogs);
projectRouter.post("/:projectId/remove-deployment", handleRemoveDeployment);
projectRouter.get("/", handleProjectAll);
projectRouter.post("/", handleProjectCreate);
+
app.use("/api/project", projectRouter); // Mount the authenticated router
app.use("/", express.static("../front/dist"));
diff --git a/apps/canvas/back/src/project_monitor.ts b/apps/canvas/back/src/project_monitor.ts
index c0ea56c..b494854 100644
--- a/apps/canvas/back/src/project_monitor.ts
+++ b/apps/canvas/back/src/project_monitor.ts
@@ -75,6 +75,25 @@
hasLogs(): boolean {
return this.logs.size > 0;
}
+
+ async reloadWorker(workerId: string): Promise<void> {
+ const workerAddress = this.workers.get(workerId);
+ if (!workerAddress) {
+ throw new Error(`Worker ${workerId} not found in service ${this.serviceName}`);
+ }
+ try {
+ const response = await fetch(`${workerAddress}/update`, { method: "POST" });
+ if (!response.ok) {
+ throw new Error(
+ `Failed to trigger reload for worker ${workerId} at ${workerAddress}: ${response.statusText}`,
+ );
+ }
+ console.log(`Reload triggered for worker ${workerId} in service ${this.serviceName}`);
+ } catch (error) {
+ console.error(`Error reloading worker ${workerId} in service ${this.serviceName}:`, error);
+ throw error; // Re-throw to be caught by ProjectMonitor
+ }
+ }
}
export class ProjectMonitor {
@@ -131,4 +150,12 @@
}
return new Map();
}
+
+ async reloadWorker(serviceName: string, workerId: string): Promise<void> {
+ const serviceMonitor = this.serviceMonitors.get(serviceName);
+ if (!serviceMonitor) {
+ throw new Error(`Service ${serviceName} not found`);
+ }
+ await serviceMonitor.reloadWorker(workerId);
+ }
}
diff --git a/apps/canvas/front/src/Monitoring.tsx b/apps/canvas/front/src/Monitoring.tsx
index 659d1a1..6f7446c 100644
--- a/apps/canvas/front/src/Monitoring.tsx
+++ b/apps/canvas/front/src/Monitoring.tsx
@@ -6,7 +6,7 @@
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from "@/components/ui/resizable";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { useToast } from "@/hooks/use-toast";
-import { LogsIcon } from "lucide-react";
+import { Check, Ellipsis, LoaderCircle, LogsIcon, X, RefreshCw } from "lucide-react";
// ANSI escape sequence regex
// eslint-disable-next-line no-control-regex
@@ -16,39 +16,6 @@
return text.replace(ANSI_ESCAPE_REGEX, "");
}
-const WaitingIcon = () => (
- <svg viewBox="0 0 24 24" fill="currentColor" className="w-4 h-4 mr-2 inline-block align-middle text-gray-500">
- <circle cx="6" cy="12" r="2" />
- <circle cx="12" cy="12" r="2" />
- <circle cx="18" cy="12" r="2" />
- </svg>
-);
-const RunningIcon = () => (
- <svg
- className="animate-spin w-4 h-4 mr-2 inline-block align-middle text-blue-500"
- xmlns="http://www.w3.org/2000/svg"
- fill="none"
- viewBox="0 0 24 24"
- >
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
- <path
- className="opacity-75"
- fill="currentColor"
- d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
- ></path>
- </svg>
-);
-const SuccessIcon = () => (
- <svg viewBox="0 0 24 24" fill="currentColor" className="w-4 h-4 mr-2 inline-block align-middle text-green-500">
- <path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" />
- </svg>
-);
-const FailureIcon = () => (
- <svg viewBox="0 0 24 24" fill="currentColor" className="w-4 h-4 mr-2 inline-block align-middle text-red-500">
- <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
- </svg>
-);
-
export function Logs() {
const { toast } = useToast();
const projectId = useProjectId();
@@ -126,7 +93,6 @@
useEffect(() => {
if (sortedServices.length > 0 && sortedServices[0].workers && sortedServices[0].workers.length > 0) {
- // Only set if no logs are currently selected, to avoid overriding user interaction
if (!selectedServiceForLogs && !selectedWorkerIdForLogs) {
handleViewLogsClick(sortedServices[0].name, sortedServices[0].workers[0].id);
}
@@ -139,6 +105,37 @@
setSelectedWorkerIdForLogs(workerId);
};
+ const handleReloadWorkerClick = useCallback(
+ (serviceName: string, workerId: string) => {
+ if (!projectId) return;
+ toast({
+ title: "Worker reload initiated",
+ description: `Worker ${serviceName} / ${workerId} is reloading.`,
+ });
+ fetch(`/api/project/${projectId}/reload/${serviceName}/${workerId}`, {
+ method: "POST",
+ })
+ .then((resp) => {
+ if (!resp.ok) {
+ throw new Error(`Failed to reload worker: ${resp.statusText}`);
+ }
+ toast({
+ title: "Worker reloaded",
+ description: `Successfully reloaded worker ${serviceName} / ${workerId}`,
+ });
+ })
+ .catch((e) => {
+ console.error(e);
+ toast({
+ variant: "destructive",
+ title: "Failed to reload worker",
+ description: `Failed to reload worker ${serviceName} / ${workerId} in service`,
+ });
+ });
+ },
+ [projectId, toast],
+ );
+
const defaultExpandedServiceNames = useMemo(() => sortedServices.map((s) => s.name), [sortedServices]);
const defaultExpandedFirstWorkerId = useMemo(() => {
if (sortedServices.length > 0 && sortedServices[0].workers && sortedServices[0].workers.length > 0) {
@@ -173,7 +170,7 @@
</AccordionTrigger>
<AccordionContent className="pl-2">
<TooltipProvider>
- <div className="text-sm">
+ <div className="text-sm flex flex-col gap-1 items-start">
<Button
onClick={() =>
handleViewLogsClick(service.name, worker.id)
@@ -185,6 +182,20 @@
<LogsIcon className="w-4 h-4" />
View Logs
</Button>
+ <Button
+ onClick={() =>
+ handleReloadWorkerClick(
+ service.name,
+ worker.id,
+ )
+ }
+ size="sm"
+ variant="link"
+ className="!px-0"
+ >
+ <RefreshCw className="w-4 h-4" />
+ Reload Worker
+ </Button>
{!worker.commit && (
<p className="flex items-center">
<FailureIcon />
@@ -293,6 +304,21 @@
);
}
+function WaitingIcon(): JSX.Element {
+ return <Ellipsis className="w-4 h-4 mr-2 inline-block align-middle" />;
+}
+function RunningIcon(): JSX.Element {
+ return <LoaderCircle className="animate-spin w-4 h-4 mr-2 inline-block align-middle" />;
+}
+
+function SuccessIcon(): JSX.Element {
+ return <Check className="w-4 h-4 mr-2 inline-block align-middle" />;
+}
+
+function FailureIcon(): JSX.Element {
+ return <X className="w-4 h-4 mr-2 inline-block align-middle" />;
+}
+
function CommandStateIcon({ state }: { state: string | undefined }): JSX.Element | null {
switch (state?.toLowerCase()) {
case "running":
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index df658af..c86199f 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -476,6 +476,8 @@
height: number;
};
+let refreshEnvIntervalId: number | null = null;
+
export type AppState = {
projectId: string | undefined;
mode: "edit" | "deploy";
@@ -577,6 +579,54 @@
});
};
+ const startRefreshEnvInterval = () => {
+ if (refreshEnvIntervalId) {
+ clearInterval(refreshEnvIntervalId);
+ }
+ if (get().projectId && typeof document !== "undefined" && document.visibilityState === "visible") {
+ console.log("Starting refreshEnv interval for project:", get().projectId);
+ refreshEnvIntervalId = setInterval(async () => {
+ if (get().projectId && typeof document !== "undefined" && document.visibilityState === "visible") {
+ console.log("Interval: Calling refreshEnv for project:", get().projectId);
+ await get().refreshEnv();
+ } else if (refreshEnvIntervalId) {
+ console.log(
+ "Interval: Conditions not met (project removed or tab hidden), stopping interval from inside.",
+ );
+ clearInterval(refreshEnvIntervalId);
+ refreshEnvIntervalId = null;
+ }
+ }, 5000) as unknown as number;
+ } else {
+ console.log(
+ "Not starting refreshEnv interval. Project ID:",
+ get().projectId,
+ "Visibility:",
+ typeof document !== "undefined" ? document.visibilityState : "SSR",
+ );
+ }
+ };
+
+ const stopRefreshEnvInterval = () => {
+ if (refreshEnvIntervalId) {
+ console.log("Stopping refreshEnv interval for project:", get().projectId);
+ clearInterval(refreshEnvIntervalId);
+ refreshEnvIntervalId = null;
+ }
+ };
+
+ if (typeof document !== "undefined") {
+ document.addEventListener("visibilitychange", () => {
+ if (document.visibilityState === "visible") {
+ console.log("Tab became visible, attempting to start refreshEnv interval.");
+ startRefreshEnvInterval();
+ } else {
+ console.log("Tab became hidden, stopping refreshEnv interval.");
+ stopRefreshEnvInterval();
+ }
+ });
+ }
+
const injectNetworkNodes = () => {
const newNetworks = get().env.networks.filter(
(x) => !get().nodes.some((n) => n.type === "network" && n.data.domain === x.domain),
@@ -896,7 +946,6 @@
refreshEnv: async () => {
const projectId = get().projectId;
let env: Env = defaultEnv;
-
try {
if (projectId) {
const response = await fetch(`/api/project/${projectId}/env`);
@@ -916,7 +965,6 @@
if (JSON.stringify(get().env) !== JSON.stringify(env)) {
set({ env });
injectNetworkNodes();
-
if (env.integrations.github) {
set({ githubService: new GitHubServiceImpl(projectId!) });
} else {
@@ -929,9 +977,11 @@
set({ mode });
},
setProject: async (projectId) => {
- if (projectId === get().projectId) {
+ const currentProjectId = get().projectId;
+ if (projectId === currentProjectId) {
return;
}
+ stopRefreshEnvInterval();
set({
projectId,
});
@@ -943,10 +993,13 @@
set({ mode: "edit" });
}
restoreSaved();
+ startRefreshEnvInterval();
} else {
set({
nodes: [],
edges: [],
+ env: defaultEnv,
+ githubService: null,
});
}
},