mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
feat: reveal agent install tokens separately
Open the existing token reveal dialog after manual agent-token creation and provide a responsive Show token only action so Docker and Compose users can copy PULSE_TOKEN without extracting it from a command. Also normalize two pre-existing frontend formatting drifts required by the full formatting gate. Refs #1775 Contract-Neutral: Token-only reveal reuses the existing security token result and dialog without changing API, persistence, or install command contracts. Change-source: pulse-maintainer
This commit is contained in:
@@ -1,48 +1,39 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "9ddfc69ec71d8db6b313522f54c2f68db88ec914",
|
||||
"verified_at": "2026-08-26T22:30:00Z",
|
||||
"base_sha": "ee35d178b5c7dad09877c20bf20acd2aba8f0b51",
|
||||
"verified_at": "2026-08-26T22:53:00Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx",
|
||||
"frontend-modern/src/components/Workloads/diskListModel.ts",
|
||||
"frontend-modern/src/components/shared/DetailSectionTable.tsx",
|
||||
"frontend-modern/src/components/shared/ProgressBar.tsx",
|
||||
"frontend-modern/src/components/shared/detailSectionModel.ts"
|
||||
"frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx",
|
||||
"frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx": "21bf8fd35d7c79f9bceade2addf8a952a10dfac724a90d3f892769d1d3dbdde1",
|
||||
"frontend-modern/src/components/Workloads/diskListModel.ts": "7e446cbc5b6b87abb12af69e4dc5a78ae26258ffbbc8f5f70235924d3d535084",
|
||||
"frontend-modern/src/components/shared/DetailSectionTable.tsx": "eff0236a3d524a041aefe18fa021cc619f61337fb9b80e74bbea07789a90e69e",
|
||||
"frontend-modern/src/components/shared/ProgressBar.tsx": "8fda21a7dd22a1c00557686604dc07d7b69011d690a9618cf480eaf939e61103",
|
||||
"frontend-modern/src/components/shared/detailSectionModel.ts": "42a6f8e70a03303d366efe8f8d490b1bf1bc8bfd6338d0510db35890d19ddeec"
|
||||
"frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx": "b323df464d7d42fea5b38f553afce74cf3d56c4459199f95a45fc4d48ef17084",
|
||||
"frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx": "e7fbdac65fe0ec225bdf898eac4bad112cdbd490322d37861677faf4be6ae74e"
|
||||
},
|
||||
"routes": [
|
||||
"/proxmox/overview"
|
||||
"/settings/infrastructure?add=docker"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 768,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"height": 844
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"multi-filesystem Proxmox guest expanded to its Overview tab on desktop",
|
||||
"same expanded filesystem state in the narrow table layout at tablet width",
|
||||
"same expanded filesystem state at phone width with wrapped detail text"
|
||||
"Docker agent installer immediately after manually generating an install token",
|
||||
"token-only reveal reopened from the generated-token confirmation",
|
||||
"token-only reveal at phone width"
|
||||
],
|
||||
"interactions": [
|
||||
"expanded checkout-web-373 and confirmed both known filesystems retain percentage, used/total, and type text",
|
||||
"confirmed each filesystem exposes an accessible progress value matching its rendered fill",
|
||||
"resized through 1440x900, 768x900, and 390x844 and confirmed bar alignment, readable wrapping, and zero document-level horizontal overflow",
|
||||
"reloaded the rebased current build and reconfirmed matching progress values plus zero document-level horizontal overflow at desktop and phone widths"
|
||||
"generated a fresh Docker agent install token and confirmed the token-only dialog opened automatically",
|
||||
"confirmed the dialog exposes the token value and Copy token action without an install command",
|
||||
"dismissed the token dialog and reopened it with Show token only without minting another credential",
|
||||
"confirmed the token wraps, the copy action spans the mobile card, and the page has no horizontal overflow at 390x844",
|
||||
"confirmed the built frontend produced no browser console errors during the verified flows"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -380,29 +380,39 @@ export const InfrastructureInstallerSection: Component<InfrastructureInstallerSe
|
||||
</div>
|
||||
|
||||
<Show when={state.latestRecord()}>
|
||||
<div class="flex items-center gap-2 rounded-md border border-blue-200 bg-blue-50 px-4 py-2 text-xs text-blue-800 dark:border-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
<svg
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
<div class="flex flex-col gap-3 rounded-md border border-blue-200 bg-blue-50 px-4 py-3 text-xs text-blue-800 dark:border-blue-800 dark:bg-blue-900 dark:text-blue-200 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span>
|
||||
{state.latestTokenSource() === 'setup_handoff' ? (
|
||||
<>
|
||||
First-host install token <strong>{state.latestRecord()?.name}</strong>{' '}
|
||||
prepared automatically. Commands below already include this credential.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Install token <strong>{state.latestRecord()?.name}</strong> created. Commands
|
||||
below now include this credential.
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
class="shrink-0 self-start sm:self-auto"
|
||||
onClick={state.showCurrentTokenOnly}
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span>
|
||||
{state.latestTokenSource() === 'setup_handoff' ? (
|
||||
<>
|
||||
First-host install token <strong>{state.latestRecord()?.name}</strong> prepared
|
||||
automatically. Commands below already include this credential.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Install token <strong>{state.latestRecord()?.name}</strong> created. Commands
|
||||
below now include this credential.
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
Show token only
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
+1
@@ -261,6 +261,7 @@ describe('infrastructure operations model', () => {
|
||||
);
|
||||
expect(infrastructureInstallerSectionSource).toContain('Generate install token');
|
||||
expect(infrastructureInstallerSectionSource).toContain('Generate token');
|
||||
expect(infrastructureInstallerSectionSource).toContain('Show token only');
|
||||
expect(infrastructureInstallerSectionSource).toContain(
|
||||
'This is the Pulse Agent handoff from first-run setup inside Add infrastructure.',
|
||||
);
|
||||
|
||||
+29
@@ -27,6 +27,7 @@ const mocks = vi.hoisted(() => ({
|
||||
notificationSuccess: vi.fn(),
|
||||
notificationError: vi.fn(),
|
||||
notificationInfo: vi.fn(),
|
||||
showTokenReveal: vi.fn(),
|
||||
loggerError: vi.fn(),
|
||||
loggerWarn: vi.fn(),
|
||||
navigate: vi.fn(),
|
||||
@@ -69,6 +70,10 @@ vi.mock('@/stores/notifications', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/stores/tokenReveal', () => ({
|
||||
showTokenReveal: mocks.showTokenReveal,
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
logger: {
|
||||
error: mocks.loggerError,
|
||||
@@ -194,6 +199,30 @@ describe('useInfrastructureOperationsState command-building closures', () => {
|
||||
const cmd = state.getUninstallCommand();
|
||||
expect(cmd).toContain("--token 'tok-1'");
|
||||
expect(cmd).not.toContain('<api-token>');
|
||||
expect(mocks.showTokenReveal).toHaveBeenCalledWith({
|
||||
token: 'tok-1',
|
||||
record: expect.objectContaining({ id: 'rec-1', name: 'Agent' }),
|
||||
source: 'agent',
|
||||
note: expect.stringContaining('PULSE_TOKEN or Compose environment configuration'),
|
||||
});
|
||||
dispose();
|
||||
});
|
||||
|
||||
it('reopens the token-only reveal without minting another credential', async () => {
|
||||
const { state, dispose } = mountHook();
|
||||
await flushAsync();
|
||||
await state.handleGenerateToken();
|
||||
mocks.showTokenReveal.mockClear();
|
||||
|
||||
state.showCurrentTokenOnly();
|
||||
|
||||
expect(mocks.createHostAgentInstallToken).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.showTokenReveal).toHaveBeenCalledWith({
|
||||
token: 'tok-1',
|
||||
record: expect.objectContaining({ id: 'rec-1', name: 'Agent' }),
|
||||
source: 'agent',
|
||||
note: expect.stringContaining('PULSE_TOKEN or Compose environment configuration'),
|
||||
});
|
||||
dispose();
|
||||
});
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MonitoringAPI } from '@/api/monitoring';
|
||||
import { NodesAPI } from '@/api/nodes';
|
||||
import { SecurityAPI, type APITokenRecord } from '@/api/security';
|
||||
import { notificationStore } from '@/stores/notifications';
|
||||
import { showTokenReveal } from '@/stores/tokenReveal';
|
||||
import type { AgentLookupResponse, ConnectedInfrastructureItem } from '@/types/api';
|
||||
import type { SecurityStatus } from '@/types/config';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
@@ -45,6 +46,8 @@ const SETUP_HANDOFF_INSTALL_STEPS = new Set<InfrastructurePanelStep>([
|
||||
'docker',
|
||||
'kubernetes',
|
||||
]);
|
||||
const INSTALL_TOKEN_REVEAL_NOTE =
|
||||
'Copy just this token for PULSE_TOKEN or Compose environment configuration. The generated install commands remain available after you close this dialog.';
|
||||
|
||||
const isActiveInfrastructureItem = (item: ConnectedInfrastructureItem) => item.status === 'active';
|
||||
|
||||
@@ -245,6 +248,19 @@ Pulse prepares the first-host install token from setup so you can move straight
|
||||
notificationStore.success('Confirmed install commands without an API token.', 3500);
|
||||
};
|
||||
|
||||
const showCurrentTokenOnly = () => {
|
||||
const token = currentToken();
|
||||
const record = latestRecord();
|
||||
if (!token || !record) return;
|
||||
|
||||
showTokenReveal({
|
||||
token,
|
||||
record,
|
||||
source: 'agent',
|
||||
note: INSTALL_TOKEN_REVEAL_NOTE,
|
||||
});
|
||||
};
|
||||
|
||||
const generateInstallToken = async (
|
||||
source: 'manual' | 'setup_handoff',
|
||||
options: { notifySuccess?: boolean } = {},
|
||||
@@ -271,6 +287,14 @@ Pulse prepares the first-host install token from setup so you can move straight
|
||||
setTokenName('');
|
||||
setConfirmedNoToken(false);
|
||||
setSetupHandoffAutoTokenFailed(false);
|
||||
if (source === 'manual') {
|
||||
showTokenReveal({
|
||||
token,
|
||||
record,
|
||||
source: 'agent',
|
||||
note: INSTALL_TOKEN_REVEAL_NOTE,
|
||||
});
|
||||
}
|
||||
if (options.notifySuccess) {
|
||||
notificationStore.success(
|
||||
withCommands
|
||||
@@ -606,6 +630,7 @@ Pulse prepares the first-host install token from setup so you can move straight
|
||||
setupHandoff,
|
||||
setupHandoffAutoTokenFailed,
|
||||
setupHandoffAutoTokenPending,
|
||||
showCurrentTokenOnly,
|
||||
tokenName,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -42,8 +42,16 @@ export const mergeDeliveryLogRows = (
|
||||
heldEvents: AlertEvent[],
|
||||
): DeliveryLogRow[] => {
|
||||
const rows: DeliveryLogRow[] = [
|
||||
...entries.map((entry): DeliveryLogRow => ({ kind: 'attempt', timestamp: entry.timestamp, entry })),
|
||||
...heldEvents.map((event): DeliveryLogRow => ({ kind: 'held', timestamp: event.occurredAt, event })),
|
||||
...entries.map((entry): DeliveryLogRow => ({
|
||||
kind: 'attempt',
|
||||
timestamp: entry.timestamp,
|
||||
entry,
|
||||
})),
|
||||
...heldEvents.map((event): DeliveryLogRow => ({
|
||||
kind: 'held',
|
||||
timestamp: event.occurredAt,
|
||||
event,
|
||||
})),
|
||||
];
|
||||
return rows.sort((a, b) => {
|
||||
const at = new Date(a.timestamp).getTime() || 0;
|
||||
|
||||
@@ -43,8 +43,13 @@ export function useAlertDestinationsTabState(props: AlertDestinationsTabStatePro
|
||||
deliveryNeedsAttention,
|
||||
loadDeliveryHealth,
|
||||
} = useNotificationDeliveryHealth();
|
||||
const { deliveryLog, deliveryLogUnavailable, refreshingDeliveryLog, heldEvents, loadDeliveryLog } =
|
||||
useNotificationDeliveryLog();
|
||||
const {
|
||||
deliveryLog,
|
||||
deliveryLogUnavailable,
|
||||
refreshingDeliveryLog,
|
||||
heldEvents,
|
||||
loadDeliveryLog,
|
||||
} = useNotificationDeliveryLog();
|
||||
const webhookState = useAlertWebhookDestinationsState();
|
||||
|
||||
const isLoading = createMemo(
|
||||
|
||||
Reference in New Issue
Block a user