mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Clarify first-run unlock guidance
This commit is contained in:
@@ -78,50 +78,52 @@ work extends shared components instead of creating new local variants.
|
||||
48. `frontend-modern/src/features/`
|
||||
49. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx`
|
||||
50. `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx`
|
||||
51. `frontend-modern/src/components/SetupWizard/__tests__/SetupWizard.test.tsx`
|
||||
52. `frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsx`
|
||||
53. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx`
|
||||
54. `frontend-modern/src/components/Settings/SystemLogsPanel.tsx`
|
||||
55. `frontend-modern/src/components/Settings/useSystemLogsPanelState.ts`
|
||||
56. `frontend-modern/src/utils/systemLogsPresentation.ts`
|
||||
57. `frontend-modern/src/components/Settings/__tests__/SystemLogsPanel.test.tsx`
|
||||
58. `frontend-modern/src/features/operations/OperationsPageSurface.tsx`
|
||||
59. `frontend-modern/src/features/operations/operationsPageModel.ts`
|
||||
60. `frontend-modern/src/pages/Operations.tsx`
|
||||
61. `frontend-modern/src/components/Settings/ResourcePicker.tsx`
|
||||
62. `frontend-modern/src/components/Settings/reportingResourceTypes.ts`
|
||||
63. `frontend-modern/src/utils/reportableResourceTypes.ts`
|
||||
64. `frontend-modern/src/utils/reportingResourceTypes.ts`
|
||||
65. `frontend-modern/src/utils/problemResourcePresentation.ts`
|
||||
66. `frontend-modern/src/utils/dashboardEmptyStatePresentation.ts`
|
||||
67. `frontend-modern/src/utils/dashboardGuestPresentation.ts`
|
||||
68. `frontend-modern/src/utils/dashboardKpiPresentation.ts`
|
||||
69. `frontend-modern/src/utils/dashboardTrendPresentation.ts`
|
||||
70. `frontend-modern/src/components/Toast/Toast.tsx`
|
||||
71. `frontend-modern/src/utils/toast.ts`
|
||||
72. `frontend-modern/src/utils/semanticTonePresentation.ts`
|
||||
73. `frontend-modern/src/utils/emptyStatePresentation.ts`
|
||||
74. `frontend-modern/src/utils/typeColumnPresentation.ts`
|
||||
61. `frontend-modern/src/pages/__tests__/Operations.helpers.test.ts`
|
||||
59. `frontend-modern/src/components/Settings/NetworkDiscoverySection.tsx`
|
||||
60. `frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx`
|
||||
61. `frontend-modern/src/components/Settings/networkSettingsModel.ts`
|
||||
62. `frontend-modern/src/components/Settings/useDiscoverySettingsState.ts`
|
||||
63. `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`
|
||||
64. `frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx`
|
||||
65. `frontend-modern/src/components/Settings/settingsPanelRegistryLoaders.ts`
|
||||
66. `frontend-modern/src/components/Settings/settingsNavigationModel.ts`
|
||||
67. `frontend-modern/src/components/Settings/settingsNavCatalog.ts`
|
||||
68. `frontend-modern/src/components/Settings/settingsNavVisibility.ts`
|
||||
69. `frontend-modern/src/components/Settings/settingsRouting.ts`
|
||||
70. `frontend-modern/src/components/Settings/settingsTabSaveBehavior.ts`
|
||||
71. `frontend-modern/src/components/Settings/settingsTypes.ts`
|
||||
72. `frontend-modern/src/components/Settings/useSettingsNavigation.ts`
|
||||
73. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx`
|
||||
74. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx`
|
||||
75. `frontend-modern/src/components/Settings/DockerRuntimeSettingsCard.tsx`
|
||||
76. `frontend-modern/src/components/shared/EnvironmentLockBadge.tsx`
|
||||
77. `frontend-modern/src/utils/environmentLockPresentation.ts`
|
||||
51. `frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx`
|
||||
52. `frontend-modern/src/components/SetupWizard/__tests__/SetupWizard.test.tsx`
|
||||
53. `frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsx`
|
||||
54. `frontend-modern/src/components/SetupWizard/__tests__/WelcomeStep.test.tsx`
|
||||
55. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx`
|
||||
56. `frontend-modern/src/components/Settings/SystemLogsPanel.tsx`
|
||||
57. `frontend-modern/src/components/Settings/useSystemLogsPanelState.ts`
|
||||
58. `frontend-modern/src/utils/systemLogsPresentation.ts`
|
||||
59. `frontend-modern/src/components/Settings/__tests__/SystemLogsPanel.test.tsx`
|
||||
60. `frontend-modern/src/features/operations/OperationsPageSurface.tsx`
|
||||
61. `frontend-modern/src/features/operations/operationsPageModel.ts`
|
||||
62. `frontend-modern/src/pages/Operations.tsx`
|
||||
63. `frontend-modern/src/components/Settings/ResourcePicker.tsx`
|
||||
64. `frontend-modern/src/components/Settings/reportingResourceTypes.ts`
|
||||
65. `frontend-modern/src/utils/reportableResourceTypes.ts`
|
||||
66. `frontend-modern/src/utils/reportingResourceTypes.ts`
|
||||
67. `frontend-modern/src/utils/problemResourcePresentation.ts`
|
||||
68. `frontend-modern/src/utils/dashboardEmptyStatePresentation.ts`
|
||||
69. `frontend-modern/src/utils/dashboardGuestPresentation.ts`
|
||||
70. `frontend-modern/src/utils/dashboardKpiPresentation.ts`
|
||||
71. `frontend-modern/src/utils/dashboardTrendPresentation.ts`
|
||||
72. `frontend-modern/src/components/Toast/Toast.tsx`
|
||||
73. `frontend-modern/src/utils/toast.ts`
|
||||
74. `frontend-modern/src/utils/semanticTonePresentation.ts`
|
||||
75. `frontend-modern/src/utils/emptyStatePresentation.ts`
|
||||
76. `frontend-modern/src/utils/typeColumnPresentation.ts`
|
||||
77. `frontend-modern/src/pages/__tests__/Operations.helpers.test.ts`
|
||||
78. `frontend-modern/src/components/Settings/NetworkDiscoverySection.tsx`
|
||||
79. `frontend-modern/src/components/Settings/NetworkBoundarySettingsSection.tsx`
|
||||
80. `frontend-modern/src/components/Settings/networkSettingsModel.ts`
|
||||
81. `frontend-modern/src/components/Settings/useDiscoverySettingsState.ts`
|
||||
82. `frontend-modern/src/components/Settings/useSettingsInfrastructurePanelProps.ts`
|
||||
83. `frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx`
|
||||
84. `frontend-modern/src/components/Settings/settingsPanelRegistryLoaders.ts`
|
||||
85. `frontend-modern/src/components/Settings/settingsNavigationModel.ts`
|
||||
86. `frontend-modern/src/components/Settings/settingsNavCatalog.ts`
|
||||
87. `frontend-modern/src/components/Settings/settingsNavVisibility.ts`
|
||||
88. `frontend-modern/src/components/Settings/settingsRouting.ts`
|
||||
89. `frontend-modern/src/components/Settings/settingsTabSaveBehavior.ts`
|
||||
90. `frontend-modern/src/components/Settings/settingsTypes.ts`
|
||||
91. `frontend-modern/src/components/Settings/useSettingsNavigation.ts`
|
||||
92. `frontend-modern/src/components/Settings/useSettingsPanelRegistry.tsx`
|
||||
93. `frontend-modern/src/components/Settings/useSettingsSystemPanels.tsx`
|
||||
94. `frontend-modern/src/components/Settings/DockerRuntimeSettingsCard.tsx`
|
||||
95. `frontend-modern/src/components/shared/EnvironmentLockBadge.tsx`
|
||||
96. `frontend-modern/src/utils/environmentLockPresentation.ts`
|
||||
|
||||
## Shared Boundaries
|
||||
|
||||
@@ -171,13 +173,20 @@ work extends shared components instead of creating new local variants.
|
||||
completion CTA language aligned with the governed infrastructure install
|
||||
workspace instead of regressing to a route jump that leaves the next action
|
||||
implicit.
|
||||
8. Keep the settings-shell infrastructure landing path aligned with that same
|
||||
8. Keep the first welcome screen in
|
||||
`frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx`
|
||||
explicit about operator context. The shell must explain that the bootstrap
|
||||
token only unlocks first-run setup, state where the command should run, and
|
||||
adapt command/help text to detected Docker or containerized deployments
|
||||
instead of assuming the operator already knows which host or container owns
|
||||
the Pulse install.
|
||||
9. Keep the settings-shell infrastructure landing path aligned with that same
|
||||
first-session story. `frontend-modern/src/components/Settings/settingsNavigationModel.ts`
|
||||
must treat `/settings` and the infrastructure settings tab as the canonical
|
||||
path to `/settings/infrastructure/install`, not to reporting/control, so
|
||||
the shell does not send first-time operators to the wrong infrastructure
|
||||
subview by default.
|
||||
9. Keep dashboard onboarding copy on the shared presentation owner in
|
||||
10. Keep dashboard onboarding copy on the shared presentation owner in
|
||||
`frontend-modern/src/utils/dashboardEmptyStatePresentation.ts`. Both the
|
||||
infrastructure empty state and the dashboard route's no-resources state
|
||||
must name the canonical install workspace explicitly and expose the same
|
||||
|
||||
@@ -59,7 +59,7 @@ export const SetupWizard: Component<SetupWizardProps> = (props) => {
|
||||
setCurrentStep('completion');
|
||||
};
|
||||
|
||||
const stepLabels = ['Unlock', 'Security', 'Install'];
|
||||
const stepLabels = ['Unlock server', 'Security', 'Install host'];
|
||||
|
||||
return (
|
||||
<div class="min-h-screen bg-base flex flex-col" role="main" aria-label="Pulse Setup Wizard">
|
||||
|
||||
@@ -47,12 +47,18 @@ describe('SetupWizard', () => {
|
||||
|
||||
render(() => <SetupWizard onComplete={onComplete} />);
|
||||
|
||||
expect(screen.getByText('Step indicator 0:Unlock > Security > Install')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Step indicator 0:Unlock server > Security > Install host'),
|
||||
).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Welcome next' }));
|
||||
expect(screen.getByText('Step indicator 1:Unlock > Security > Install')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Step indicator 1:Unlock server > Security > Install host'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Security complete' }));
|
||||
expect(screen.getByText('Step indicator 2:Unlock > Security > Install')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Step indicator 2:Unlock server > Security > Install host'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Completion finish' })).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Completion finish' }));
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, render, screen, waitFor } from '@solidjs/testing-library';
|
||||
import { WelcomeStep } from '../steps/WelcomeStep';
|
||||
|
||||
const apiFetchMock = vi.fn();
|
||||
const apiFetchJSONMock = vi.fn();
|
||||
const copyToClipboardMock = vi.fn();
|
||||
const showErrorMock = vi.fn();
|
||||
const showSuccessMock = vi.fn();
|
||||
|
||||
vi.mock('@/utils/apiClient', () => ({
|
||||
apiFetch: (...args: unknown[]) => apiFetchMock(...args),
|
||||
apiFetchJSON: (...args: unknown[]) => apiFetchJSONMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/clipboard', () => ({
|
||||
copyToClipboard: (...args: unknown[]) => copyToClipboardMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/toast', () => ({
|
||||
showError: (...args: unknown[]) => showErrorMock(...args),
|
||||
showSuccess: (...args: unknown[]) => showSuccessMock(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('WelcomeStep', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
copyToClipboardMock.mockResolvedValue(true);
|
||||
apiFetchJSONMock.mockResolvedValue({
|
||||
bootstrapTokenPath: '/etc/pulse/.bootstrap_token',
|
||||
isDocker: false,
|
||||
inContainer: false,
|
||||
lxcCtid: '',
|
||||
dockerContainerName: '',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('frames first-run setup as a clear three-step journey', async () => {
|
||||
render(() => (
|
||||
<WelcomeStep
|
||||
onNext={vi.fn()}
|
||||
bootstrapToken=""
|
||||
setBootstrapToken={vi.fn()}
|
||||
isUnlocked={false}
|
||||
setIsUnlocked={vi.fn()}
|
||||
/>
|
||||
));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/security/status');
|
||||
});
|
||||
|
||||
expect(screen.getByText('Unlock this Pulse server')).toBeInTheDocument();
|
||||
expect(screen.getByText('Create the admin account')).toBeInTheDocument();
|
||||
expect(screen.getByText('Install the first host')).toBeInTheDocument();
|
||||
expect(screen.getByText('What this token does')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'This one-time bootstrap token only unlocks first-run setup on this Pulse server. It is not your admin password and it is not the API token you will use after setup.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Verify bootstrap token →' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders environment-aware Docker unlock guidance when the runtime reports a container name', async () => {
|
||||
apiFetchJSONMock.mockResolvedValue({
|
||||
bootstrapTokenPath: '/srv/pulse/bootstrap.token',
|
||||
isDocker: true,
|
||||
inContainer: false,
|
||||
lxcCtid: '',
|
||||
dockerContainerName: 'pulse-main',
|
||||
});
|
||||
|
||||
render(() => (
|
||||
<WelcomeStep
|
||||
onNext={vi.fn()}
|
||||
bootstrapToken=""
|
||||
setBootstrapToken={vi.fn()}
|
||||
isUnlocked={false}
|
||||
setIsUnlocked={vi.fn()}
|
||||
/>
|
||||
));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Docker deployment')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Pulse appears to be running in Docker as container "pulse-main". Run the command on the Docker host so you can read /srv/pulse/bootstrap.token from that container.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('docker exec pulse-main cat /srv/pulse/bootstrap.token')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, createSignal, Show } from 'solid-js';
|
||||
import { Component, createMemo, createSignal, onMount, Show } from 'solid-js';
|
||||
import { showError, showSuccess } from '@/utils/toast';
|
||||
import { apiFetch, apiFetchJSON } from '@/utils/apiClient';
|
||||
import { logger } from '@/utils/logger';
|
||||
@@ -19,6 +19,7 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
const [isDocker, setIsDocker] = createSignal(false);
|
||||
const [inContainer, setInContainer] = createSignal(false);
|
||||
const [lxcCtid, setLxcCtid] = createSignal('');
|
||||
const [dockerContainerName, setDockerContainerName] = createSignal('');
|
||||
const [copied, setCopied] = createSignal(false);
|
||||
|
||||
const copyCommand = async () => {
|
||||
@@ -40,20 +41,23 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
isDocker?: boolean;
|
||||
inContainer?: boolean;
|
||||
lxcCtid?: string;
|
||||
dockerContainerName?: string;
|
||||
}>('/api/security/status');
|
||||
if (data?.bootstrapTokenPath) {
|
||||
setTokenPath(data.bootstrapTokenPath);
|
||||
setIsDocker(data.isDocker || false);
|
||||
setInContainer(data.inContainer || false);
|
||||
setLxcCtid(data.lxcCtid || '');
|
||||
setDockerContainerName(data.dockerContainerName || '');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch bootstrap info:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Call on component load
|
||||
fetchBootstrapInfo();
|
||||
onMount(() => {
|
||||
void fetchBootstrapInfo();
|
||||
});
|
||||
|
||||
const handleUnlock = async () => {
|
||||
if (!props.bootstrapToken.trim()) {
|
||||
@@ -85,7 +89,7 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
const getTokenCommand = () => {
|
||||
const path = tokenPath() || '/etc/pulse/.bootstrap_token';
|
||||
if (isDocker()) {
|
||||
return `docker exec <container> cat ${path}`;
|
||||
return `docker exec ${dockerContainerName() || '<pulse-container>'} cat ${path}`;
|
||||
}
|
||||
if (inContainer() && lxcCtid()) {
|
||||
return `pct exec ${lxcCtid()} -- cat ${path}`;
|
||||
@@ -96,6 +100,42 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
return `cat ${path}`;
|
||||
};
|
||||
|
||||
const deploymentLabel = createMemo(() => {
|
||||
if (isDocker()) {
|
||||
return 'Docker deployment';
|
||||
}
|
||||
if (inContainer() && lxcCtid()) {
|
||||
return 'LXC container';
|
||||
}
|
||||
if (inContainer()) {
|
||||
return 'Containerized deployment';
|
||||
}
|
||||
return 'Direct host install';
|
||||
});
|
||||
|
||||
const deploymentHint = createMemo(() => {
|
||||
const path = tokenPath() || '/etc/pulse/.bootstrap_token';
|
||||
if (isDocker()) {
|
||||
return dockerContainerName()
|
||||
? `Pulse appears to be running in Docker as container "${dockerContainerName()}". Run the command on the Docker host so you can read ${path} from that container.`
|
||||
: `Pulse appears to be running in Docker. Run the command on the Docker host and replace <pulse-container> with the running Pulse container name.`;
|
||||
}
|
||||
if (inContainer() && lxcCtid()) {
|
||||
return `Pulse appears to be running in LXC container ${lxcCtid()}. Run the command on the Proxmox host so you can execute into that container and read ${path}.`;
|
||||
}
|
||||
if (inContainer()) {
|
||||
return `Pulse appears to be running in a containerized environment. Run the command from the host that manages the container so you can read ${path}.`;
|
||||
}
|
||||
return `Run the command directly in a shell on the Pulse server to read ${path}.`;
|
||||
});
|
||||
|
||||
const unlockHelp = createMemo(() => {
|
||||
if (isDocker()) {
|
||||
return 'This one-time bootstrap token only unlocks first-run setup. After verification, you will create the admin account and Pulse will generate the long-lived API token separately.';
|
||||
}
|
||||
return 'This one-time bootstrap token only unlocks first-run setup on this Pulse server. It is not your admin password and it is not the API token you will use after setup.';
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="text-center relative">
|
||||
{/* Logo */}
|
||||
@@ -117,6 +157,36 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-8 grid gap-3 text-left sm:grid-cols-3">
|
||||
<div class="rounded-md border border-border bg-surface px-4 py-3">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
|
||||
Step 1
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-semibold text-base-content">Unlock this Pulse server</div>
|
||||
<p class="mt-1 text-xs text-muted">
|
||||
Read the one-time bootstrap token from the system where Pulse is installed.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-md border border-border bg-surface px-4 py-3">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
|
||||
Step 2
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-semibold text-base-content">Create the admin account</div>
|
||||
<p class="mt-1 text-xs text-muted">
|
||||
Set the first login and let Pulse generate the credentials you need to save.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-md border border-border bg-surface px-4 py-3">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
|
||||
Step 3
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-semibold text-base-content">Install the first host</div>
|
||||
<p class="mt-1 text-xs text-muted">
|
||||
Open Infrastructure Install and connect the first system you want Pulse to monitor.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bootstrap token unlock */}
|
||||
<Show when={!props.isUnlocked}>
|
||||
<div class="p-8 max-w-lg mx-auto bg-surface border border-border rounded-md text-left animate-slide-up delay-300 relative group">
|
||||
@@ -125,10 +195,22 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
Unlock Setup
|
||||
</h3>
|
||||
<p class="text-sm text-muted mb-6">
|
||||
Run the following command on the machine where Pulse is installed to retrieve the
|
||||
secure bootstrap token:
|
||||
Run the following command on the Pulse server to retrieve the one-time bootstrap
|
||||
token that unlocks this wizard:
|
||||
</p>
|
||||
|
||||
<div class="mb-4 rounded-md border border-blue-200 bg-blue-50 px-4 py-3 dark:border-blue-800 dark:bg-blue-950/40">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
|
||||
Where to run it
|
||||
</div>
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-[11px] font-semibold text-blue-700 dark:bg-blue-900 dark:text-blue-200">
|
||||
{deploymentLabel()}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-blue-900 dark:text-blue-100">{deploymentHint()}</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<div class="bg-base rounded-md p-4 font-mono text-sm text-emerald-400 border border-border-subtle flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3 overflow-x-auto scrollbar-hide">
|
||||
@@ -147,6 +229,13 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 rounded-md border border-emerald-200 bg-emerald-50 px-4 py-3 dark:border-emerald-800 dark:bg-emerald-950/40">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-emerald-700 dark:text-emerald-300">
|
||||
What this token does
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-emerald-900 dark:text-emerald-100">{unlockHelp()}</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
@@ -163,12 +252,17 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
}, 400);
|
||||
}
|
||||
}}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleUnlock()}
|
||||
onKeyDown={(e) => e.key === 'Enter' && void handleUnlock()}
|
||||
class="w-full px-5 py-3.5 bg-surface border border-border rounded-md text-base-content placeholder-slate-400 focus:outline-none focus:ring-0 focus:border-blue-500 transition-colors font-mono"
|
||||
placeholder="Paste your bootstrap token"
|
||||
autofocus
|
||||
/>
|
||||
|
||||
<p class="text-xs text-muted">
|
||||
After Pulse verifies this token, the next step is creating the admin account for
|
||||
this server.
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={handleUnlock}
|
||||
disabled={isValidating() || !props.bootstrapToken.trim()}
|
||||
@@ -195,10 +289,10 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
Validating...
|
||||
Verifying bootstrap token...
|
||||
</>
|
||||
) : (
|
||||
'Continue to Setup →'
|
||||
'Verify bootstrap token →'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@@ -212,7 +306,7 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
onClick={props.onNext}
|
||||
class="py-4 px-10 bg-blue-600 hover:bg-blue-700 text-white text-lg font-medium rounded-md transition-colors duration-200"
|
||||
>
|
||||
Get Started →
|
||||
Continue to Security →
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
Reference in New Issue
Block a user