1ade52825c
Track in-progress deploys in the client store so the progress modal opens on click without waiting for the applications list refetch. Show deleting state as a blurred glass overlay on table rows and service cards instead of replacing row content. Co-authored-by: Cursor <cursoragent@cursor.com>
184 lines
5.9 KiB
TypeScript
184 lines
5.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useMemo } from 'react';
|
|
import { usePathname } from 'next/navigation';
|
|
import { useQuery, useQueries, keepPreviousData } from '@tanstack/react-query';
|
|
import api from '@/lib/api';
|
|
import type { Application } from '@/types';
|
|
import { useAuthStore } from '@/lib/store';
|
|
import { useDeployProgressStore } from '@/lib/deploy-progress-store';
|
|
import { getAppsInProgress, isActiveBuildProgress } from '@/lib/deployment-progress';
|
|
import { filterApplications, filterManagedServices } from '@/lib/product-type';
|
|
import { BuildProgressModal, type BuildProgress } from '@/components/build-progress-modal';
|
|
import { DeploymentProgressBar } from '@/components/deployment-progress-bar';
|
|
|
|
function stubAppFromTrack(appId: string, appName: string): Application {
|
|
return {
|
|
id: appId,
|
|
name: appName,
|
|
runtime: 'nodejs',
|
|
databaseType: 'none',
|
|
cpuRequest: '',
|
|
cpuLimit: '',
|
|
memoryRequest: '',
|
|
memoryLimit: '',
|
|
replicas: 1,
|
|
port: 3000,
|
|
userId: '',
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
deployments: [
|
|
{
|
|
id: `pending-${appId}`,
|
|
status: 'building',
|
|
imageTag: '',
|
|
applicationId: appId,
|
|
triggeredBy: 'user',
|
|
createdAt: new Date().toISOString(),
|
|
},
|
|
],
|
|
} as Application;
|
|
}
|
|
|
|
export function DeploymentProgressManager() {
|
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
|
const pathname = usePathname();
|
|
const { minimized, focusedAppId, tracked, minimize, expand, stopTracking } =
|
|
useDeployProgressStore();
|
|
|
|
const pollAppsList = (list: Application[] | undefined) => {
|
|
const pending = useDeployProgressStore.getState().tracked.length;
|
|
return getAppsInProgress(list ?? []).length > 0 || pending > 0 ? 3000 : false;
|
|
};
|
|
|
|
const { data: appsList = [] } = useQuery<Application[]>({
|
|
queryKey: ['applications', 'application'],
|
|
queryFn: () => api.get('/applications', { params: { productType: 'application' } }).then((r) => r.data),
|
|
enabled: isAuthenticated,
|
|
refetchInterval: (query) => pollAppsList(query.state.data),
|
|
});
|
|
|
|
const { data: servicesList = [] } = useQuery<Application[]>({
|
|
queryKey: ['applications', 'managed'],
|
|
queryFn: () => api.get('/applications', { params: { productType: 'managed' } }).then((r) => r.data),
|
|
enabled: isAuthenticated,
|
|
refetchInterval: (query) => pollAppsList(query.state.data),
|
|
});
|
|
|
|
const allResources = useMemo(
|
|
() => [...filterApplications(appsList), ...filterManagedServices(servicesList)],
|
|
[appsList, servicesList],
|
|
);
|
|
|
|
const deployingFromList = useMemo(() => getAppsInProgress(allResources), [allResources]);
|
|
|
|
const deployingApps = useMemo(() => {
|
|
const byId = new Map<string, Application>();
|
|
for (const app of deployingFromList) {
|
|
byId.set(app.id, app);
|
|
}
|
|
for (const t of tracked) {
|
|
if (!byId.has(t.appId)) {
|
|
const fromList = allResources.find((a) => a.id === t.appId);
|
|
byId.set(t.appId, fromList ?? stubAppFromTrack(t.appId, t.appName));
|
|
}
|
|
}
|
|
return Array.from(byId.values());
|
|
}, [deployingFromList, tracked, allResources]);
|
|
|
|
const progressQueries = useQueries({
|
|
queries: deployingApps.map((app) => ({
|
|
queryKey: ['build-progress', app.id],
|
|
queryFn: () =>
|
|
api
|
|
.get<{ progress: BuildProgress | null }>(`/deployments/applications/${app.id}/build-progress`)
|
|
.then((r) => r.data.progress),
|
|
refetchInterval: 1500,
|
|
placeholderData: keepPreviousData,
|
|
enabled: isAuthenticated,
|
|
})),
|
|
});
|
|
|
|
const activeItems = useMemo(() => {
|
|
return deployingApps.map((app, i) => ({
|
|
app,
|
|
progress: progressQueries[i]?.data ?? null,
|
|
}));
|
|
}, [deployingApps, progressQueries]);
|
|
|
|
const routeAppId =
|
|
pathname.match(/\/dashboard\/apps\/([^/]+)/)?.[1] ??
|
|
pathname.match(/\/dashboard\/services\/([^/]+)/)?.[1];
|
|
|
|
useEffect(() => {
|
|
if (deployingApps.length === 0) {
|
|
useDeployProgressStore.setState({ minimized: false, focusedAppId: null, tracked: [] });
|
|
}
|
|
}, [deployingApps.length]);
|
|
|
|
useEffect(() => {
|
|
if (activeItems.length === 0) return;
|
|
const { focusedAppId: current } = useDeployProgressStore.getState();
|
|
const ids = new Set(activeItems.map((item) => item.app.id));
|
|
if (current && ids.has(current)) return;
|
|
const preferred =
|
|
(routeAppId && ids.has(routeAppId) ? routeAppId : null) ?? activeItems[0].app.id;
|
|
useDeployProgressStore.setState({ focusedAppId: preferred });
|
|
}, [activeItems, routeAppId]);
|
|
|
|
useEffect(() => {
|
|
for (const { app, progress } of activeItems) {
|
|
if (!progress) continue;
|
|
if (progress.phase === 'done' || progress.phase === 'failed' || progress.phase === 'cancelled') {
|
|
const stillBuilding = deployingFromList.some((a) => a.id === app.id);
|
|
if (!stillBuilding) {
|
|
stopTracking(app.id);
|
|
}
|
|
}
|
|
}
|
|
}, [activeItems, deployingFromList, stopTracking]);
|
|
|
|
const focusedItem =
|
|
activeItems.find((item) => item.app.id === focusedAppId) ?? activeItems[0];
|
|
|
|
const focusedProgress =
|
|
focusedItem?.progress ??
|
|
({ phase: 'building', percent: 0, message: 'Starting deployment…' } satisfies BuildProgress);
|
|
|
|
const showModal =
|
|
!minimized &&
|
|
!!focusedItem &&
|
|
(!focusedItem.progress ||
|
|
isActiveBuildProgress(focusedItem.progress) ||
|
|
focusedItem.progress.phase === 'done');
|
|
|
|
const handleMinimize = () => {
|
|
if (focusedItem) minimize(focusedItem.app.id);
|
|
};
|
|
|
|
const handleExpand = (appId: string) => {
|
|
expand(appId);
|
|
};
|
|
|
|
if (deployingApps.length === 0) return null;
|
|
|
|
return (
|
|
<>
|
|
{minimized && (
|
|
<div className="sticky top-0 z-40">
|
|
<DeploymentProgressBar items={activeItems} onExpand={handleExpand} />
|
|
</div>
|
|
)}
|
|
|
|
{showModal && (
|
|
<BuildProgressModal
|
|
appId={focusedItem.app.id}
|
|
appName={focusedItem.app.name}
|
|
progress={focusedProgress}
|
|
onMinimize={handleMinimize}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|