mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-22 03:02:35 +00:00
parent
a34bfcc037
commit
3dbe638f70
7 changed files with 124 additions and 211 deletions
|
|
@ -1321,6 +1321,11 @@ Its primary job is rapid v5-to-v6 reorientation. The modal should explain
|
|||
where familiar v5 destinations moved and what each v6 top-level area is for,
|
||||
so operators who feel briefly lost after upgrading can rebuild their mental
|
||||
map in one pass.
|
||||
That guided welcome surface should stay compact. The canonical shape is a
|
||||
coachmark-sized card centered on the current destination with one short
|
||||
step-specific sentence, a small clickable step strip, and minimal footer
|
||||
controls. It must not grow back into a large sectioned explainer when one
|
||||
sentence would do the job.
|
||||
The guided stop map inside that welcome surface is interactive, not decorative:
|
||||
operators must be able to jump directly to any tour step from the stop list,
|
||||
and desktop layouts may widen the panel enough to keep step labels readable
|
||||
|
|
@ -1334,8 +1339,9 @@ use bordered flat fills and normal app radii instead of gradient washes,
|
|||
glassmorphism, or other marketing-style promo chrome that drifts from the rest
|
||||
of the product.
|
||||
Secondary disclosures such as telemetry must stay subordinate to that
|
||||
orientation job: keep them compact, link to the canonical privacy/settings
|
||||
surfaces, and do not let them crowd out the migration wayfinding copy.
|
||||
orientation job: keep them as footer-level links into the canonical
|
||||
privacy/settings surfaces, and do not let them crowd out the migration
|
||||
wayfinding copy.
|
||||
That state owner now also owns public-demo suppression: the modal must stay
|
||||
closed until `sessionPresentationPolicyResolved()` is true and must fail closed
|
||||
when `presentationPolicyIsDemoMode()` resolves true, so the public demo does
|
||||
|
|
|
|||
|
|
@ -1143,11 +1143,11 @@ describe('shared primitive guardrails', () => {
|
|||
expect(whatsNewModalStateSource).toContain('spotlightStyle');
|
||||
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_FEATURE_CARDS');
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_TELEMETRY_TITLE');
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_URL');
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_PRIVACY_URL');
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_LABEL');
|
||||
expect(whatsNewModalModelSource).toContain('MIGRATION_GUIDE_DOC_URL');
|
||||
expect(whatsNewModalModelSource).toContain('Telemetry details');
|
||||
expect(whatsNewModalModelSource).toContain("title: 'Dashboard'");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,13 +5,10 @@ import ServerIcon from 'lucide-solid/icons/server';
|
|||
import BoxesIcon from 'lucide-solid/icons/boxes';
|
||||
import HardDriveIcon from 'lucide-solid/icons/hard-drive';
|
||||
import ShieldCheckIcon from 'lucide-solid/icons/shield-check';
|
||||
import ChartBarIcon from 'lucide-solid/icons/chart-bar';
|
||||
import ExternalLinkIcon from 'lucide-solid/icons/external-link';
|
||||
import XIcon from 'lucide-solid/icons/x';
|
||||
import {
|
||||
WHATS_NEW_BACK_LABEL,
|
||||
WHATS_NEW_CLOSE_LABEL,
|
||||
WHATS_NEW_CURRENT_STEP_LABEL,
|
||||
WHATS_NEW_DOCS_LABEL,
|
||||
WHATS_NEW_DOCS_URL,
|
||||
WHATS_NEW_DO_NOT_SHOW_LABEL,
|
||||
|
|
@ -19,17 +16,9 @@ import {
|
|||
WHATS_NEW_KICKER_LABEL,
|
||||
WHATS_NEW_NEXT_LABEL,
|
||||
WHATS_NEW_PRIMARY_ACTION_LABEL,
|
||||
WHATS_NEW_PROGRESS_PREFIX,
|
||||
WHATS_NEW_PRIVACY_URL,
|
||||
WHATS_NEW_SKIP_LABEL,
|
||||
WHATS_NEW_STEP_MAP_HELPER,
|
||||
WHATS_NEW_STEP_MAP_LABEL,
|
||||
WHATS_NEW_SUBTITLE,
|
||||
WHATS_NEW_TELEMETRY_COPY,
|
||||
WHATS_NEW_TELEMETRY_ENV_VAR,
|
||||
WHATS_NEW_TELEMETRY_LABEL,
|
||||
WHATS_NEW_TELEMETRY_PRIVACY_LABEL,
|
||||
WHATS_NEW_TELEMETRY_SETTINGS_PATH,
|
||||
WHATS_NEW_TELEMETRY_TITLE,
|
||||
WHATS_NEW_TELEMETRY_LINK_LABEL,
|
||||
WHATS_NEW_TITLE,
|
||||
type WhatsNewFeatureCard,
|
||||
} from './whatsNewModalModel';
|
||||
|
|
@ -86,24 +75,31 @@ export function WhatsNewModal() {
|
|||
data-tour-step={step().target}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="whats-new-title"
|
||||
aria-label={WHATS_NEW_TITLE}
|
||||
tabindex="-1"
|
||||
class="fixed z-[1001] max-h-[min(90vh,44rem)] overflow-y-auto rounded-md border border-border bg-surface shadow-xl focus:outline-none"
|
||||
class="fixed z-[1001] max-h-[min(90vh,32rem)] overflow-y-auto rounded-md border border-border bg-surface shadow-xl focus:outline-none"
|
||||
style={state.panelStyle()}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div class="flex items-start justify-between border-b border-border bg-surface px-6 py-5">
|
||||
<div>
|
||||
<div class="inline-flex items-center rounded-full border border-border bg-surface-hover px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted">
|
||||
{WHATS_NEW_KICKER_LABEL}
|
||||
<div class="flex items-start justify-between border-b border-border bg-surface px-5 py-4">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted">
|
||||
<span class="inline-flex items-center rounded-full border border-border bg-surface-hover px-2 py-1">
|
||||
{WHATS_NEW_KICKER_LABEL}
|
||||
</span>
|
||||
<span>
|
||||
{WHATS_NEW_PROGRESS_PREFIX} {state.stepIndex() + 1} of {state.stepCount()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-3 text-xs font-semibold uppercase tracking-[0.18em] text-muted">
|
||||
Step {state.stepIndex() + 1} of {state.stepCount()}
|
||||
<div class="mt-3 flex items-start gap-3">
|
||||
<div class={`flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md border bg-surface ${step().accent}`}>
|
||||
<WhatsNewFeatureIcon card={step()} />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-lg font-semibold text-base-content">{step().title}</div>
|
||||
<p class="mt-1 text-sm leading-6 text-muted">{step().description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<h2 id="whats-new-title" class="mt-1 text-xl font-semibold text-base-content">
|
||||
{WHATS_NEW_TITLE}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">{WHATS_NEW_SUBTITLE}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={state.handleClose}
|
||||
|
|
@ -115,148 +111,81 @@ export function WhatsNewModal() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5 px-6 py-5">
|
||||
<div class={`rounded-md border p-4 ${step().accent}`}>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-md border border-white/40 bg-surface text-inherit dark:border-slate-800">
|
||||
<WhatsNewFeatureIcon card={step()} />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] opacity-70">
|
||||
{WHATS_NEW_CURRENT_STEP_LABEL}
|
||||
</div>
|
||||
<div class="mt-1 text-base font-semibold text-inherit">{step().title}</div>
|
||||
<p class="mt-2 text-sm leading-6 text-inherit">{step().description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2.5">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted">
|
||||
{WHATS_NEW_STEP_MAP_LABEL}
|
||||
</div>
|
||||
<div class="h-px flex-1 bg-border"></div>
|
||||
</div>
|
||||
<p class="text-xs text-muted">{WHATS_NEW_STEP_MAP_HELPER}</p>
|
||||
<div class="grid grid-cols-2 gap-2.5">
|
||||
<For each={WHATS_NEW_FEATURE_CARDS}>
|
||||
{(card, index) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => state.handleSelectStep(index())}
|
||||
aria-current={index() === state.stepIndex() ? 'step' : undefined}
|
||||
class={`min-h-[3.25rem] rounded-md border px-3 py-2.5 text-left transition-colors ${
|
||||
<div class="space-y-4 px-5 py-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<For each={WHATS_NEW_FEATURE_CARDS}>
|
||||
{(card, index) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => state.handleSelectStep(index())}
|
||||
aria-current={index() === state.stepIndex() ? 'step' : undefined}
|
||||
class={`inline-flex items-center gap-2 rounded-md border px-2.5 py-1.5 text-xs font-medium transition-colors ${
|
||||
index() === state.stepIndex()
|
||||
? 'border-blue-300 bg-blue-50 text-blue-900 dark:border-blue-700 dark:bg-blue-950 dark:text-blue-100'
|
||||
: 'border-border bg-surface-hover text-base-content hover:border-slate-300 hover:bg-surface dark:hover:border-slate-700'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
class={`inline-flex h-5 w-5 items-center justify-center rounded-sm border text-[10px] font-semibold font-mono ${
|
||||
index() === state.stepIndex()
|
||||
? 'border-blue-300 bg-blue-50 text-blue-900 dark:border-blue-700 dark:bg-blue-950 dark:text-blue-100'
|
||||
: 'border-border bg-surface-hover text-base-content hover:border-slate-300 hover:bg-surface dark:hover:border-slate-700'
|
||||
? 'border-blue-200 bg-surface text-blue-700 dark:border-blue-800 dark:bg-slate-900 dark:text-blue-200'
|
||||
: 'border-border bg-surface text-muted'
|
||||
}`}
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class={`flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border text-[11px] font-semibold font-mono ${
|
||||
index() === state.stepIndex()
|
||||
? 'border-blue-200 bg-surface text-blue-700 dark:border-blue-800 dark:bg-slate-900 dark:text-blue-200'
|
||||
: 'border-border bg-surface text-muted'
|
||||
}`}
|
||||
>
|
||||
{String(index() + 1).padStart(2, '0')}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 truncate text-sm font-semibold">{card.title}</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-border bg-surface-hover p-3.5">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="mt-0.5 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted">
|
||||
<ChartBarIcon class="h-4 w-4" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-[0.16em] text-muted">
|
||||
{WHATS_NEW_TELEMETRY_LABEL}
|
||||
</div>
|
||||
<div class="mt-1 text-sm font-medium text-base-content">
|
||||
{WHATS_NEW_TELEMETRY_TITLE}
|
||||
</div>
|
||||
<p class="mt-1 text-xs leading-5 text-muted">
|
||||
{WHATS_NEW_TELEMETRY_COPY[0]}{' '}
|
||||
<a
|
||||
href={WHATS_NEW_PRIVACY_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="underline hover:text-base-content"
|
||||
>
|
||||
{WHATS_NEW_TELEMETRY_PRIVACY_LABEL}
|
||||
</a>
|
||||
</p>
|
||||
<p class="mt-1 text-xs leading-5 text-muted">
|
||||
{WHATS_NEW_TELEMETRY_COPY[1]}
|
||||
</p>
|
||||
<div class="mt-2 flex flex-wrap items-center gap-2 text-[10px] text-muted">
|
||||
<span class="rounded bg-surface px-1.5 py-0.5 font-medium">
|
||||
{WHATS_NEW_TELEMETRY_SETTINGS_PATH}
|
||||
{String(index() + 1).padStart(2, '0')}
|
||||
</span>
|
||||
<code class="rounded bg-surface px-1.5 py-0.5 font-mono">
|
||||
{WHATS_NEW_TELEMETRY_ENV_VAR}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span>{card.title}</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 rounded-md border border-border bg-surface-hover p-3.5 sm:flex-row sm:items-center sm:justify-between">
|
||||
<label class="flex items-center gap-2 text-sm text-muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={state.dontShowAgain()}
|
||||
onChange={(event) => state.setDontShowAgain(event.currentTarget.checked)}
|
||||
class="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
{WHATS_NEW_DO_NOT_SHOW_LABEL}
|
||||
</label>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex flex-col gap-3 border-t border-border pt-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex flex-wrap items-center gap-3 text-xs text-muted">
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={state.dontShowAgain()}
|
||||
onChange={(event) => state.setDontShowAgain(event.currentTarget.checked)}
|
||||
class="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
{WHATS_NEW_DO_NOT_SHOW_LABEL}
|
||||
</label>
|
||||
<a
|
||||
href={WHATS_NEW_DOCS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-border bg-surface px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
class="underline hover:text-base-content"
|
||||
>
|
||||
{WHATS_NEW_DOCS_LABEL}
|
||||
<ExternalLinkIcon class="h-4 w-4" />
|
||||
</a>
|
||||
<a
|
||||
href={WHATS_NEW_PRIVACY_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="underline hover:text-base-content"
|
||||
>
|
||||
{WHATS_NEW_TELEMETRY_LINK_LABEL}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between border-t border-border bg-surface-hover px-6 py-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.handleClose}
|
||||
class="text-sm font-medium text-muted transition-colors hover:text-base-content"
|
||||
>
|
||||
{WHATS_NEW_SKIP_LABEL}
|
||||
</button>
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.handlePrevious}
|
||||
disabled={state.isFirstStep()}
|
||||
class="rounded-md border border-border px-3.5 py-2 text-sm font-medium text-base-content transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{WHATS_NEW_BACK_LABEL}
|
||||
</button>
|
||||
<button
|
||||
onClick={state.handleNext}
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700"
|
||||
type="button"
|
||||
>
|
||||
{state.isLastStep() ? WHATS_NEW_PRIMARY_ACTION_LABEL : WHATS_NEW_NEXT_LABEL}
|
||||
</button>
|
||||
<div class="flex items-center gap-2 sm:justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={state.handlePrevious}
|
||||
disabled={state.isFirstStep()}
|
||||
class="rounded-md border border-border px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{WHATS_NEW_BACK_LABEL}
|
||||
</button>
|
||||
<button
|
||||
onClick={state.handleNext}
|
||||
class="rounded-md bg-blue-600 px-3.5 py-1.5 text-sm font-semibold text-white transition-colors hover:bg-blue-700"
|
||||
type="button"
|
||||
>
|
||||
{state.isLastStep() ? WHATS_NEW_PRIMARY_ACTION_LABEL : WHATS_NEW_NEXT_LABEL}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -51,16 +51,14 @@ describe('WhatsNewModal', () => {
|
|||
expect(whatsNewModalStateSource).toContain('handleClose');
|
||||
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_FEATURE_CARDS');
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_TELEMETRY_TITLE');
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_URL');
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_PRIVACY_URL');
|
||||
expect(whatsNewModalModelSource).toContain('MIGRATION_GUIDE_DOC_URL');
|
||||
expect(whatsNewModalModelSource).toContain('PRIVACY_DOC_URL');
|
||||
expect(whatsNewModalModelSource).toContain('anonymous daily ping');
|
||||
expect(whatsNewModalModelSource).toContain('nothing is gone');
|
||||
expect(whatsNewModalModelSource).toContain('Telemetry details');
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_KICKER_LABEL');
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_STEP_MAP_LABEL');
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_TELEMETRY_LABEL');
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_PROGRESS_PREFIX');
|
||||
expect(whatsNewModalModelSource).toContain("WHATS_NEW_PRIMARY_ACTION_LABEL = 'Done'");
|
||||
expect(whatsNewModalModelSource).not.toContain('https://github.com/rcourtman/Pulse/blob/main/docs/README.md');
|
||||
expect(whatsNewModalModelSource).not.toContain('https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md');
|
||||
expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_LABEL');
|
||||
|
|
@ -70,14 +68,14 @@ describe('WhatsNewModal', () => {
|
|||
it('renders when the navigation modal has not been seen yet', async () => {
|
||||
render(() => <WhatsNewModal />);
|
||||
|
||||
const dialog = await screen.findByRole('dialog');
|
||||
const dialog = await screen.findByRole('dialog', { name: 'Welcome to Pulse v6' });
|
||||
expect(dialog).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('Welcome to Pulse v6')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText(/nothing is gone/i)).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('Step 1 of 5')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText('Where Things Moved')).toBeInTheDocument();
|
||||
expect(within(dialog).queryByText(/Stop 1/i)).not.toBeInTheDocument();
|
||||
expect(within(dialog).getAllByText('Dashboard')).toHaveLength(2);
|
||||
expect(within(dialog).getByText('V5 to V6')).toBeInTheDocument();
|
||||
expect(within(dialog).getByText(/overview for health, alerts, capacity/i)).toBeInTheDocument();
|
||||
expect(within(dialog).queryByText('Where Things Moved')).not.toBeInTheDocument();
|
||||
expect(within(dialog).getByRole('link', { name: 'Migration guide' })).toBeInTheDocument();
|
||||
expect(within(dialog).getByRole('link', { name: 'Telemetry details' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('stays hidden for public demo sessions', async () => {
|
||||
|
|
@ -130,26 +128,26 @@ describe('WhatsNewModal', () => {
|
|||
it('advances through the guided tour and finishes on the last step', async () => {
|
||||
render(() => <WhatsNewModal />);
|
||||
|
||||
expect(await screen.findByText(/overall picture: health, alerts, capacity/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/overview for health, alerts, capacity/i)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
|
||||
expect(await screen.findByText(/systems themselves: Proxmox nodes, Docker hosts/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/Systems live here: nodes, hosts, clusters/i)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
|
||||
expect(await screen.findByText(/guests or Docker workloads in v5/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/If you looked for guests in v5/i)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
|
||||
expect(await screen.findByText(/Datastores, pools, disks, and capacity moved here/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/Datastores, pools, disks, and capacity live here/i)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Next' }));
|
||||
expect(await screen.findByText(/Backups, snapshots, and replication moved here/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: "Let's go" })).toBeInTheDocument();
|
||||
expect(await screen.findByText(/Backups, snapshots, and replication live here/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Done' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('lets the user jump to a tour stop directly from the stop map', async () => {
|
||||
render(() => <WhatsNewModal />);
|
||||
|
||||
expect(await screen.findByText(/overall picture: health, alerts, capacity/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/overview for health, alerts, capacity/i)).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /Workloads/i }));
|
||||
|
||||
expect(await screen.findByText(/guests or Docker workloads in v5/i)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/If you looked for guests in v5/i)).toBeInTheDocument();
|
||||
expect(screen.getByText('Step 3 of 5')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -169,9 +169,9 @@ export function useWhatsNewModalState() {
|
|||
|
||||
const rect = spotlightRect();
|
||||
const panel = panelRef();
|
||||
const desktopWidth = window.innerWidth >= 1024 ? 448 : 420;
|
||||
const desktopWidth = window.innerWidth >= 1024 ? 376 : 344;
|
||||
const panelWidth = Math.min(desktopWidth, window.innerWidth - 32);
|
||||
const panelHeight = panel?.offsetHeight ?? 340;
|
||||
const panelHeight = panel?.offsetHeight ?? 260;
|
||||
|
||||
if (!rect) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -14,66 +14,48 @@ export const WHATS_NEW_PRIVACY_URL = PRIVACY_DOC_URL;
|
|||
export const WHATS_NEW_FEATURE_CARDS: WhatsNewFeatureCard[] = [
|
||||
{
|
||||
accent: 'border-indigo-200 bg-indigo-50 dark:border-indigo-800 dark:bg-indigo-900',
|
||||
description:
|
||||
'Start here first when you want the overall picture: health, alerts, capacity, and recent activity across your estate.',
|
||||
description: 'Your new overview for health, alerts, capacity, and recent activity.',
|
||||
icon: 'dashboard',
|
||||
target: 'dashboard',
|
||||
title: 'Dashboard',
|
||||
},
|
||||
{
|
||||
accent: 'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900',
|
||||
description:
|
||||
'Use this when you want the systems themselves: Proxmox nodes, Docker hosts, Kubernetes clusters, PBS, PMG, TrueNAS, and other platform roots.',
|
||||
description: 'Systems live here: nodes, hosts, clusters, and other platform roots.',
|
||||
icon: 'infrastructure',
|
||||
target: 'infrastructure',
|
||||
title: 'Infrastructure',
|
||||
},
|
||||
{
|
||||
accent: 'border-purple-200 bg-purple-50 dark:border-purple-800 dark:bg-purple-900',
|
||||
description:
|
||||
'VMs, containers, and pods live here now. If you used to drill into guests or Docker workloads in v5, this is the new starting point.',
|
||||
description: 'VMs, containers, and pods live here. If you looked for guests in v5, start here.',
|
||||
icon: 'workloads',
|
||||
target: 'workloads',
|
||||
title: 'Workloads',
|
||||
},
|
||||
{
|
||||
accent: 'border-emerald-200 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-900',
|
||||
description:
|
||||
'Datastores, pools, disks, and capacity moved here so storage is one destination across platforms.',
|
||||
description: 'Datastores, pools, disks, and capacity live here across platforms.',
|
||||
icon: 'storage',
|
||||
target: 'storage',
|
||||
title: 'Storage',
|
||||
},
|
||||
{
|
||||
accent: 'border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-900',
|
||||
description:
|
||||
'Backups, snapshots, and replication moved here. Open this when you want restore posture or recent recovery activity.',
|
||||
description: 'Backups, snapshots, and replication live here.',
|
||||
icon: 'recovery',
|
||||
target: 'recovery',
|
||||
title: 'Recovery',
|
||||
},
|
||||
];
|
||||
|
||||
export const WHATS_NEW_KICKER_LABEL = 'V5 to V6 Guide';
|
||||
export const WHATS_NEW_CURRENT_STEP_LABEL = 'Now showing';
|
||||
export const WHATS_NEW_STEP_MAP_LABEL = 'Where Things Moved';
|
||||
export const WHATS_NEW_STEP_MAP_HELPER = 'Jump ahead or follow the highlighted path.';
|
||||
export const WHATS_NEW_KICKER_LABEL = 'V5 to V6';
|
||||
export const WHATS_NEW_TITLE = 'Welcome to Pulse v6';
|
||||
export const WHATS_NEW_SUBTITLE =
|
||||
"If you're coming from v5, nothing is gone. Pulse is now grouped by task so you can find things faster.";
|
||||
export const WHATS_NEW_TELEMETRY_LABEL = 'Telemetry note';
|
||||
export const WHATS_NEW_TELEMETRY_TITLE = 'Anonymous telemetry';
|
||||
export const WHATS_NEW_TELEMETRY_COPY = [
|
||||
'Pulse also sends a lightweight anonymous daily ping. No hostnames, credentials, or personal information are sent.',
|
||||
'You can turn it off any time in Settings → System → General or with PULSE_TELEMETRY=false.',
|
||||
];
|
||||
export const WHATS_NEW_TELEMETRY_SETTINGS_PATH = 'Settings → System → General';
|
||||
export const WHATS_NEW_TELEMETRY_ENV_VAR = 'PULSE_TELEMETRY=false';
|
||||
export const WHATS_NEW_TELEMETRY_PRIVACY_LABEL = 'Full details';
|
||||
export const WHATS_NEW_PROGRESS_PREFIX = 'Step';
|
||||
export const WHATS_NEW_BACK_LABEL = 'Back';
|
||||
export const WHATS_NEW_CLOSE_LABEL = 'Close';
|
||||
export const WHATS_NEW_DOCS_LABEL = 'Migration guide';
|
||||
export const WHATS_NEW_DO_NOT_SHOW_LABEL = "Don't show again";
|
||||
export const WHATS_NEW_NEXT_LABEL = 'Next';
|
||||
export const WHATS_NEW_PRIMARY_ACTION_LABEL = "Let's go";
|
||||
export const WHATS_NEW_SKIP_LABEL = 'Skip tour';
|
||||
export const WHATS_NEW_PRIMARY_ACTION_LABEL = 'Done';
|
||||
export const WHATS_NEW_TELEMETRY_LINK_LABEL = 'Telemetry details';
|
||||
|
|
|
|||
|
|
@ -151,19 +151,17 @@ test.describe('Telemetry disclosure', () => {
|
|||
const infrastructureTab = page.locator(
|
||||
'[role="tab"][title="All agents and nodes across platforms"]',
|
||||
);
|
||||
await expect(dialog.getByText('Welcome to Pulse v6')).toBeVisible();
|
||||
await expect(dialog).toHaveAttribute('aria-label', 'Welcome to Pulse v6');
|
||||
await expect(dialog.getByText('Step 1 of 5')).toBeVisible();
|
||||
await expect(dialog.getByText(/nothing is gone/i)).toBeVisible();
|
||||
await expect(dialog.getByText('V5 to V6')).toBeVisible();
|
||||
await expect(assistantLauncher).toBeHidden();
|
||||
await expect(spotlight).toHaveAttribute('data-tour-step', 'dashboard');
|
||||
await expect(dialog).toHaveAttribute('data-tour-step', 'dashboard');
|
||||
await expectSpotlightAround(spotlight, dashboardTab);
|
||||
await expect(dialog.getByText('Where Things Moved')).toBeVisible();
|
||||
await expect(dialog.getByText('Telemetry note')).toBeVisible();
|
||||
await expect(dialog.getByText(/lightweight anonymous daily ping/i)).toBeVisible();
|
||||
await expect(dialog.getByText('Settings → System → General', { exact: true })).toBeVisible();
|
||||
await expect(dialog.getByText(/overview for health, alerts, capacity/i)).toBeVisible();
|
||||
await expect(dialog.getByRole('link', { name: 'Telemetry details' })).toBeVisible();
|
||||
|
||||
const privacyLink = dialog.getByRole('link', { name: 'Full details' });
|
||||
const privacyLink = dialog.getByRole('link', { name: 'Telemetry details' });
|
||||
await expect(privacyLink).toHaveAttribute('href', '/docs/PRIVACY.md');
|
||||
await expectPopupDoc(
|
||||
page,
|
||||
|
|
@ -189,7 +187,7 @@ test.describe('Telemetry disclosure', () => {
|
|||
for (let step = 0; step < 3; step += 1) {
|
||||
await dialog.getByRole('button', { name: 'Next' }).click();
|
||||
}
|
||||
await dialog.getByRole('button', { name: "Let's go" }).click();
|
||||
await dialog.getByRole('button', { name: 'Done' }).click();
|
||||
await expect(dialog).not.toBeVisible();
|
||||
await expect(assistantLauncher).toBeVisible();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue