fix(deploy): address final review — sensitiveKeys snapshot, dirty scrubbing, transition race, refetch invalidations
- Issue 1: add List<String> sensitiveKeys as 4th field to DeploymentConfigSnapshot; populate from agentConfig.getSensitiveKeys() in DeploymentExecutor; handleRestore hydrates from snap.sensitiveKeys directly; Deployment type in apps.ts gains sensitiveKeys field - Issue 2: after createApp succeeds, refetchQueries(['apps', envSlug]) before navigate so the new app is in cache before the router renders the deployed view (eliminates transient Save- disabled flash) - Issue 3: useDeploymentPageState useEffect now uses prevServerStateRef to detect local edits; background refetches only overwrite form when no local changes are present - Issue 5: handleRedeploy invalidates dirty-state + versions queries after createDeployment resolves; handleSave invalidates dirty-state after staged save - Issue 10: DirtyStateCalculator strips volatile agentConfig keys (version, updatedAt, updatedBy, environment, application) before JSON comparison via scrubAgentConfig(); adds versionBumpDoesNotMarkDirty test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ export interface Deployment {
|
||||
jarVersionId: string;
|
||||
agentConfig: Record<string, unknown> | null;
|
||||
containerConfig: Record<string, unknown>;
|
||||
sensitiveKeys: string[] | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// ui/src/pages/AppsTab/AppDeploymentPage/hooks/useDeploymentPageState.ts
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useState, useEffect, useMemo, useRef } from 'react';
|
||||
import type { ApplicationConfig } from '../../../../api/queries/commands';
|
||||
import type { App } from '../../../../api/queries/admin/apps';
|
||||
|
||||
@@ -115,9 +115,18 @@ export function useDeploymentPageState(
|
||||
}, [app, agentConfig, envDefaults]);
|
||||
|
||||
const [form, setForm] = useState<DeploymentPageFormState>(serverState);
|
||||
const prevServerStateRef = useRef<DeploymentPageFormState>(serverState);
|
||||
|
||||
useEffect(() => {
|
||||
setForm(serverState);
|
||||
// Only overwrite form if the current form value still matches the previous
|
||||
// server state (i.e., the user has no local edits). Otherwise preserve
|
||||
// user edits through background refetches.
|
||||
setForm((current) => {
|
||||
const hadLocalEdits =
|
||||
JSON.stringify(current) !== JSON.stringify(prevServerStateRef.current);
|
||||
prevServerStateRef.current = serverState;
|
||||
return hadLocalEdits ? current : serverState;
|
||||
});
|
||||
}, [serverState]);
|
||||
|
||||
return { form, setForm, reset: () => setForm(serverState), serverState };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useParams, useLocation, useNavigate } from 'react-router';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { AlertDialog, Button, Tabs, useToast } from '@cameleer/design-system';
|
||||
import { useEnvironmentStore } from '../../../api/environment-store';
|
||||
import { useEnvironments } from '../../../api/queries/admin/environments';
|
||||
@@ -45,6 +46,7 @@ export default function AppDeploymentPage() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { toast } = useToast();
|
||||
const queryClient = useQueryClient();
|
||||
const selectedEnv = useEnvironmentStore((s) => s.environment);
|
||||
const { data: environments = [], isLoading: envLoading } = useEnvironments();
|
||||
const { data: apps = [], isLoading: appsLoading } = useApps(selectedEnv);
|
||||
@@ -218,8 +220,14 @@ export default function AppDeploymentPage() {
|
||||
|
||||
setStagedJar(null);
|
||||
|
||||
// Invalidate dirty-state so the button reflects the new saved state
|
||||
await queryClient.invalidateQueries({ queryKey: ['apps', envSlug, targetApp.slug, 'dirty-state'] });
|
||||
|
||||
if (!app) {
|
||||
// Transition to the existing-app view
|
||||
// Transition to the existing-app view — refetch apps first so the new app
|
||||
// is in the cache before the router renders the deployed view (prevents
|
||||
// the transient Save-disabled flash while useApps is loading).
|
||||
await queryClient.refetchQueries({ queryKey: ['apps', envSlug] });
|
||||
navigate(`/apps/${targetApp.slug}`);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -242,7 +250,6 @@ export default function AppDeploymentPage() {
|
||||
if (stagedJar) {
|
||||
const newVersion = await uploadJar.mutateAsync({ envSlug, appSlug: app.slug, file: stagedJar });
|
||||
versionId = newVersion.id;
|
||||
setStagedJar(null);
|
||||
} else {
|
||||
if (!currentVersion) {
|
||||
toast({
|
||||
@@ -257,6 +264,10 @@ export default function AppDeploymentPage() {
|
||||
}
|
||||
|
||||
await createDeployment.mutateAsync({ envSlug, appSlug: app.slug, appVersionId: versionId });
|
||||
setStagedJar(null);
|
||||
// Invalidate dirty-state and versions so button recomputes after deploy
|
||||
queryClient.invalidateQueries({ queryKey: ['apps', envSlug, app.slug, 'dirty-state'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['apps', envSlug, app.slug, 'versions'] });
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: 'Redeploy failed',
|
||||
@@ -344,9 +355,11 @@ export default function AppDeploymentPage() {
|
||||
: prev.variables.envVars,
|
||||
},
|
||||
sensitiveKeys: {
|
||||
sensitiveKeys: Array.isArray(a.sensitiveKeys)
|
||||
? (a.sensitiveKeys as string[])
|
||||
: prev.sensitiveKeys.sensitiveKeys,
|
||||
sensitiveKeys: Array.isArray(snap.sensitiveKeys)
|
||||
? snap.sensitiveKeys
|
||||
: Array.isArray(a.sensitiveKeys)
|
||||
? (a.sensitiveKeys as string[])
|
||||
: prev.sensitiveKeys.sensitiveKeys,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user