` must be `relative`; render all `| ` cells first,
+ * then this as the last child while deleting.
+ */
+export function DeletingTableRowOverlay({
colSpan,
message = 'Deleting…',
}: {
@@ -21,8 +31,13 @@ export function DeletingTableRowCell({
message?: string;
}) {
return (
- |
-
+
+
{message}
@@ -30,11 +45,22 @@ export function DeletingTableRowCell({
);
}
-/** Full-card overlay while delete is in progress. Parent must be `relative`. */
+/** @deprecated Use DeletingTableRowOverlay on top of visible row cells */
+export function DeletingTableRowCell({
+ colSpan,
+ message = 'Deleting…',
+}: {
+ colSpan: number;
+ message?: string;
+}) {
+ return ;
+}
+
+/** Full-card glass overlay while delete is in progress. Parent must be `relative`. */
export function DeletingCardOverlay({ message = 'Deleting…' }: { message?: string }) {
return (
diff --git a/frontend/src/components/deployment-progress-manager.tsx b/frontend/src/components/deployment-progress-manager.tsx
index 5f476db..cc53998 100644
--- a/frontend/src/components/deployment-progress-manager.tsx
+++ b/frontend/src/components/deployment-progress-manager.tsx
@@ -12,26 +12,57 @@ 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, minimize, expand } = useDeployProgressStore();
+ const { minimized, focusedAppId, tracked, minimize, expand, stopTracking } =
+ useDeployProgressStore();
- const refetchWhileDeploying = (list: Application[] | undefined) =>
- getAppsInProgress(list ?? []).length > 0 ? 3000 : false;
+ const pollAppsList = (list: Application[] | undefined) => {
+ const pending = useDeployProgressStore.getState().tracked.length;
+ return getAppsInProgress(list ?? []).length > 0 || pending > 0 ? 3000 : false;
+ };
const { data: appsList = [] } = useQuery ({
queryKey: ['applications', 'application'],
queryFn: () => api.get('/applications', { params: { productType: 'application' } }).then((r) => r.data),
enabled: isAuthenticated,
- refetchInterval: (query) => refetchWhileDeploying(query.state.data),
+ refetchInterval: (query) => pollAppsList(query.state.data),
});
const { data: servicesList = [] } = useQuery({
queryKey: ['applications', 'managed'],
queryFn: () => api.get('/applications', { params: { productType: 'managed' } }).then((r) => r.data),
enabled: isAuthenticated,
- refetchInterval: (query) => refetchWhileDeploying(query.state.data),
+ refetchInterval: (query) => pollAppsList(query.state.data),
});
const allResources = useMemo(
@@ -39,7 +70,21 @@ export function DeploymentProgressManager() {
[appsList, servicesList],
);
- const deployingApps = useMemo(() => getAppsInProgress(allResources), [allResources]);
+ const deployingFromList = useMemo(() => getAppsInProgress(allResources), [allResources]);
+
+ const deployingApps = useMemo(() => {
+ const byId = new Map();
+ 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) => ({
@@ -50,6 +95,7 @@ export function DeploymentProgressManager() {
.then((r) => r.data.progress),
refetchInterval: 1500,
placeholderData: keepPreviousData,
+ enabled: isAuthenticated,
})),
});
@@ -66,7 +112,7 @@ export function DeploymentProgressManager() {
useEffect(() => {
if (deployingApps.length === 0) {
- useDeployProgressStore.setState({ minimized: false, focusedAppId: null });
+ useDeployProgressStore.setState({ minimized: false, focusedAppId: null, tracked: [] });
}
}, [deployingApps.length]);
@@ -80,12 +126,25 @@ export function DeploymentProgressManager() {
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: 'Loading progress…' } satisfies BuildProgress);
+ ({ phase: 'building', percent: 0, message: 'Starting deployment…' } satisfies BuildProgress);
+
const showModal =
!minimized &&
!!focusedItem &&
diff --git a/frontend/src/lib/deploy-progress-store.ts b/frontend/src/lib/deploy-progress-store.ts
index d2117ed..97d93b1 100644
--- a/frontend/src/lib/deploy-progress-store.ts
+++ b/frontend/src/lib/deploy-progress-store.ts
@@ -2,9 +2,19 @@
import { create } from 'zustand';
+export type TrackedDeploy = {
+ appId: string;
+ appName: string;
+ startedAt: number;
+};
+
interface DeployProgressState {
minimized: boolean;
focusedAppId: string | null;
+ /** Optimistic deploy tracking — modal opens before applications list refetches */
+ tracked: TrackedDeploy[];
+ startDeploy: (appId: string, appName?: string) => void;
+ stopTracking: (appId: string) => void;
minimize: (appId: string) => void;
expand: (appId?: string) => void;
}
@@ -12,6 +22,25 @@ interface DeployProgressState {
export const useDeployProgressStore = create((set) => ({
minimized: false,
focusedAppId: null,
+ tracked: [],
+ startDeploy: (appId, appName) =>
+ set((state) => ({
+ minimized: false,
+ focusedAppId: appId,
+ tracked: [
+ ...state.tracked.filter((t) => t.appId !== appId),
+ { appId, appName: appName?.trim() || 'Application', startedAt: Date.now() },
+ ],
+ })),
+ stopTracking: (appId) =>
+ set((state) => {
+ const tracked = state.tracked.filter((t) => t.appId !== appId);
+ const focusedAppId =
+ state.focusedAppId === appId
+ ? tracked[0]?.appId ?? null
+ : state.focusedAppId;
+ return { tracked, focusedAppId };
+ }),
minimize: (appId) => set({ minimized: true, focusedAppId: appId }),
expand: (appId) =>
set((state) => ({
diff --git a/frontend/src/lib/use-deploy-progress-actions.ts b/frontend/src/lib/use-deploy-progress-actions.ts
new file mode 100644
index 0000000..c5f3152
--- /dev/null
+++ b/frontend/src/lib/use-deploy-progress-actions.ts
@@ -0,0 +1,29 @@
+'use client';
+
+import { useQueryClient } from '@tanstack/react-query';
+import api from '@/lib/api';
+import type { BuildProgress } from '@/components/build-progress-modal';
+import { useDeployProgressStore } from '@/lib/deploy-progress-store';
+
+/** Open deploy progress UI immediately and warm build-progress cache. */
+export function useDeployProgressActions() {
+ const queryClient = useQueryClient();
+ const startDeploy = useDeployProgressStore((s) => s.startDeploy);
+
+ const notifyDeployStarted = (appId: string, appName?: string) => {
+ startDeploy(appId, appName);
+ void queryClient.invalidateQueries({ queryKey: ['applications'] });
+ void queryClient.invalidateQueries({ queryKey: ['application', appId] });
+ void queryClient.invalidateQueries({ queryKey: ['deployments', appId] });
+ void queryClient.prefetchQuery({
+ queryKey: ['build-progress', appId],
+ queryFn: () =>
+ api
+ .get<{ progress: BuildProgress | null }>(`/deployments/applications/${appId}/build-progress`)
+ .then((r) => r.data.progress),
+ staleTime: 0,
+ });
+ };
+
+ return { notifyDeployStarted };
+}
| |