Open deploy progress immediately and improve delete row glass overlay.
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>
This commit is contained in:
@@ -12,8 +12,18 @@ export function deletingResourceMessage(
|
||||
return name ? `Deleting “${name}”…` : 'Deleting application…';
|
||||
}
|
||||
|
||||
/** Single table cell spanning the full row — content centered in the row. */
|
||||
export function DeletingTableRowCell({
|
||||
/** Applied to row cells under the glass overlay so content stays visible but blurred. */
|
||||
export const deletingRowContentClass =
|
||||
'opacity-45 blur-[2px] pointer-events-none select-none transition-[filter,opacity] duration-200';
|
||||
|
||||
const glassPanelClass =
|
||||
'flex min-h-[52px] h-full w-full items-center justify-center gap-2 bg-white/45 backdrop-blur-md text-sm font-semibold text-gray-900 ring-1 ring-inset ring-white/50';
|
||||
|
||||
/**
|
||||
* Glass overlay on a table row. Parent `<tr>` must be `relative`; render all `<td>` 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 (
|
||||
<td colSpan={colSpan} className="px-6 py-4 bg-white/80 backdrop-blur-sm" aria-live="polite" aria-busy="true">
|
||||
<div className="flex min-h-[52px] w-full items-center justify-center gap-2 text-sm font-semibold text-gray-800">
|
||||
<td
|
||||
colSpan={colSpan}
|
||||
className="!absolute inset-0 z-10 border-0 p-0 m-0 h-full w-full max-w-none bg-transparent"
|
||||
aria-live="polite"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div className={glassPanelClass}>
|
||||
<Clock className="w-4 h-4 shrink-0 animate-spin text-red-600" />
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
@@ -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 <DeletingTableRowOverlay colSpan={colSpan} message={message} />;
|
||||
}
|
||||
|
||||
/** Full-card glass overlay while delete is in progress. Parent must be `relative`. */
|
||||
export function DeletingCardOverlay({ message = 'Deleting…' }: { message?: string }) {
|
||||
return (
|
||||
<div
|
||||
className="absolute inset-0 z-20 flex items-center justify-center gap-2 rounded-xl bg-white/75 backdrop-blur-sm text-sm font-semibold text-gray-800"
|
||||
className="absolute inset-0 z-20 flex items-center justify-center gap-2 rounded-xl bg-white/45 backdrop-blur-md text-sm font-semibold text-gray-900 ring-1 ring-inset ring-white/50"
|
||||
aria-live="polite"
|
||||
aria-busy="true"
|
||||
>
|
||||
|
||||
@@ -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<Application[]>({
|
||||
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<Application[]>({
|
||||
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<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) => ({
|
||||
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user