mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-15 01:07:32 +00:00
Split shared whats new modal owners
This commit is contained in:
parent
326b02842e
commit
9420018e99
7 changed files with 233 additions and 86 deletions
|
|
@ -302,6 +302,14 @@ runtime, and `frontend-modern/src/components/shared/searchTipsPopoverModel.ts`
|
|||
owns trigger variant, label/id defaults, hover policy, and trigger/popover
|
||||
class selection. Future search-tips work should extend those owners instead of
|
||||
pushing listener lifecycle or trigger policy back into the shared shell.
|
||||
The shared what's-new modal now follows that same owner split.
|
||||
`frontend-modern/src/components/shared/WhatsNewModal.tsx` stays the render
|
||||
shell, `frontend-modern/src/components/shared/useWhatsNewModalState.ts` owns
|
||||
local-storage dismissal, session dismissal, and close behavior, and
|
||||
`frontend-modern/src/components/shared/whatsNewModalModel.ts` owns the feature
|
||||
card catalog, telemetry copy, labels, and canonical docs/privacy links. Future
|
||||
what's-new work should extend those owners instead of pushing dismissal state,
|
||||
product copy, or external links back into the shared shell.
|
||||
The shared tooltip now follows that same owner split.
|
||||
`frontend-modern/src/components/shared/Tooltip.tsx` stays the render shell and
|
||||
singleton API boundary, `frontend-modern/src/components/shared/useTooltipState.ts`
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ import mobileNavBarModelSource from '@/components/shared/mobileNavBarModel.ts?ra
|
|||
import infrastructureSelectorSource from '@/components/shared/InfrastructureSelector.tsx?raw';
|
||||
import pulseDataGridSource from '@/components/shared/PulseDataGrid.tsx?raw';
|
||||
import pulseDataGridModelSource from '@/components/shared/pulseDataGridModel.ts?raw';
|
||||
import whatsNewModalSource from '@/components/shared/WhatsNewModal.tsx?raw';
|
||||
import whatsNewModalModelSource from '@/components/shared/whatsNewModalModel.ts?raw';
|
||||
import searchFieldSource from '@/components/shared/SearchField.tsx?raw';
|
||||
import searchFieldModelSource from '@/components/shared/searchFieldModel.ts?raw';
|
||||
import searchInputSource from '@/components/shared/SearchInput.tsx?raw';
|
||||
|
|
@ -54,6 +56,7 @@ import infrastructureDetailsDrawerStateSource from '@/components/shared/useInfra
|
|||
import mobileNavBarStateSource from '@/components/shared/useMobileNavBarState.ts?raw';
|
||||
import infrastructureSelectorStateSource from '@/components/shared/useInfrastructureSelectorState.ts?raw';
|
||||
import pulseDataGridStateSource from '@/components/shared/usePulseDataGridState.ts?raw';
|
||||
import whatsNewModalStateSource from '@/components/shared/useWhatsNewModalState.ts?raw';
|
||||
import searchFieldStateSource from '@/components/shared/useSearchFieldState.ts?raw';
|
||||
import searchInputStateSource from '@/components/shared/useSearchInputState.ts?raw';
|
||||
import searchTipsPopoverStateSource from '@/components/shared/useSearchTipsPopoverState.ts?raw';
|
||||
|
|
@ -513,6 +516,31 @@ describe('shared primitive guardrails', () => {
|
|||
expect(searchTipsPopoverModelSource).toContain('shouldSearchTipsPopoverOpenOnHover');
|
||||
});
|
||||
|
||||
it('keeps whats new modal on shell, runtime, and model owners', () => {
|
||||
expect(whatsNewModalSource).toContain('useWhatsNewModalState');
|
||||
expect(whatsNewModalSource).toContain('WHATS_NEW_FEATURE_CARDS');
|
||||
expect(whatsNewModalSource).not.toContain('createLocalStorageBooleanSignal');
|
||||
expect(whatsNewModalSource).not.toContain('createSignal');
|
||||
expect(whatsNewModalSource).not.toContain('WHATS_NEW_NAV_V2_SHOWN');
|
||||
expect(whatsNewModalSource).not.toContain('Documentation');
|
||||
expect(whatsNewModalSource).not.toContain(
|
||||
'https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md',
|
||||
);
|
||||
|
||||
expect(whatsNewModalStateSource).toContain('export function useWhatsNewModalState');
|
||||
expect(whatsNewModalStateSource).toContain('createLocalStorageBooleanSignal');
|
||||
expect(whatsNewModalStateSource).toContain('createSignal');
|
||||
expect(whatsNewModalStateSource).toContain('STORAGE_KEYS.WHATS_NEW_NAV_V2_SHOWN');
|
||||
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('WHATS_NEW_DOCS_LABEL');
|
||||
expect(whatsNewModalModelSource).toContain("title: 'Infrastructure'");
|
||||
});
|
||||
|
||||
it('keeps tooltip on shell, runtime, and model owners', () => {
|
||||
expect(tooltipSource).toContain('useTooltipState');
|
||||
expect(tooltipSource).toContain('createTooltipSystemState');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { createSignal } from 'solid-js';
|
||||
import { createLocalStorageBooleanSignal, STORAGE_KEYS } from '@/utils/localStorage';
|
||||
import { For } from 'solid-js';
|
||||
import { Dialog } from '@/components/shared/Dialog';
|
||||
import ServerIcon from 'lucide-solid/icons/server';
|
||||
import BoxesIcon from 'lucide-solid/icons/boxes';
|
||||
|
|
@ -8,31 +7,46 @@ 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_CLOSE_LABEL,
|
||||
WHATS_NEW_DOCS_LABEL,
|
||||
WHATS_NEW_DOCS_URL,
|
||||
WHATS_NEW_DO_NOT_SHOW_LABEL,
|
||||
WHATS_NEW_FEATURE_CARDS,
|
||||
WHATS_NEW_PRIMARY_ACTION_LABEL,
|
||||
WHATS_NEW_PRIVACY_URL,
|
||||
WHATS_NEW_RECOVERY_LINK_LABEL,
|
||||
WHATS_NEW_SUBTITLE,
|
||||
WHATS_NEW_TELEMETRY_COPY,
|
||||
WHATS_NEW_TELEMETRY_ENV_VAR,
|
||||
WHATS_NEW_TELEMETRY_PRIVACY_LABEL,
|
||||
WHATS_NEW_TELEMETRY_SETTINGS_PATH,
|
||||
WHATS_NEW_TELEMETRY_TITLE,
|
||||
WHATS_NEW_TITLE,
|
||||
type WhatsNewFeatureCard,
|
||||
} from './whatsNewModalModel';
|
||||
import { useWhatsNewModalState } from './useWhatsNewModalState';
|
||||
|
||||
const DOCS_URL = 'https://github.com/rcourtman/Pulse/blob/main/docs/README.md';
|
||||
function WhatsNewFeatureIcon(props: { card: WhatsNewFeatureCard }) {
|
||||
switch (props.card.icon) {
|
||||
case 'infrastructure':
|
||||
return <ServerIcon class="h-4 w-4" />;
|
||||
case 'workloads':
|
||||
return <BoxesIcon class="h-4 w-4" />;
|
||||
case 'storage':
|
||||
return <HardDriveIcon class="h-4 w-4" />;
|
||||
case 'recovery':
|
||||
return <ShieldCheckIcon class="h-4 w-4" />;
|
||||
}
|
||||
}
|
||||
|
||||
export function WhatsNewModal() {
|
||||
const [hasSeen, setHasSeen] = createLocalStorageBooleanSignal(
|
||||
STORAGE_KEYS.WHATS_NEW_NAV_V2_SHOWN,
|
||||
false,
|
||||
);
|
||||
const [dontShowAgain, setDontShowAgain] = createSignal(true);
|
||||
const [dismissedForSession, setDismissedForSession] = createSignal(false);
|
||||
|
||||
const isOpen = () => !hasSeen() && !dismissedForSession();
|
||||
|
||||
const handleClose = () => {
|
||||
if (dontShowAgain()) {
|
||||
setHasSeen(true);
|
||||
return;
|
||||
}
|
||||
setDismissedForSession(true);
|
||||
};
|
||||
const state = useWhatsNewModalState();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={isOpen()}
|
||||
onClose={handleClose}
|
||||
isOpen={state.isOpen()}
|
||||
onClose={state.handleClose}
|
||||
panelClass="max-w-2xl"
|
||||
ariaLabelledBy="whats-new-title"
|
||||
>
|
||||
|
|
@ -40,16 +54,14 @@ export function WhatsNewModal() {
|
|||
<div class="flex-shrink-0 flex items-start justify-between border-b border-border px-6 py-4">
|
||||
<div>
|
||||
<h2 id="whats-new-title" class="text-xl sm:text-2xl font-semibold text-base-content">
|
||||
Welcome to the New Navigation!
|
||||
{WHATS_NEW_TITLE}
|
||||
</h2>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Everything is now organized by what you want to do, not where the data comes from.
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-muted">{WHATS_NEW_SUBTITLE}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
onClick={state.handleClose}
|
||||
class="rounded-md p-1.5 text-slate-400 transition-colors hover:bg-surface-hover hover:text-muted"
|
||||
aria-label="Close"
|
||||
aria-label={WHATS_NEW_CLOSE_LABEL}
|
||||
type="button"
|
||||
>
|
||||
<XIcon class="h-5 w-5" />
|
||||
|
|
@ -58,72 +70,39 @@ export function WhatsNewModal() {
|
|||
|
||||
<div class="flex-1 overflow-y-auto space-y-4 sm:space-y-6 px-4 sm:px-6 py-4 sm:py-5">
|
||||
<div class="grid gap-3 sm:gap-4 sm:grid-cols-2">
|
||||
<div class="rounded-md border border-blue-200 bg-blue-50 p-3 sm:p-4 dark:border-blue-800 dark:bg-blue-900">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-blue-900 dark:text-blue-100">
|
||||
<ServerIcon class="h-4 w-4" />
|
||||
Infrastructure
|
||||
</div>
|
||||
<p class="mt-1.5 sm:mt-2 text-xs text-blue-900 dark:text-blue-100">
|
||||
Proxmox nodes, agents, and container runtimes live together in one unified view.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-purple-200 bg-purple-50 p-3 sm:p-4 dark:border-purple-800 dark:bg-purple-900">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-purple-900 dark:text-purple-100">
|
||||
<BoxesIcon class="h-4 w-4" />
|
||||
Workloads
|
||||
</div>
|
||||
<p class="mt-1.5 sm:mt-2 text-xs text-purple-900 dark:text-purple-100">
|
||||
All VMs, containers, and Kubernetes workloads now share a single list.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-emerald-200 bg-emerald-50 p-3 sm:p-4 dark:border-emerald-800 dark:bg-emerald-900">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-emerald-900 dark:text-emerald-100">
|
||||
<HardDriveIcon class="h-4 w-4" />
|
||||
Storage
|
||||
</div>
|
||||
<p class="mt-1.5 sm:mt-2 text-xs text-emerald-900 dark:text-emerald-100">
|
||||
Storage is now a top-level destination across all systems.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-amber-200 bg-amber-50 p-3 sm:p-4 dark:border-amber-800 dark:bg-amber-900">
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-amber-900 dark:text-amber-100">
|
||||
<ShieldCheckIcon class="h-4 w-4" />
|
||||
Recovery
|
||||
</div>
|
||||
<p class="mt-1.5 sm:mt-2 text-xs text-amber-900 dark:text-amber-100">
|
||||
Recovery events (backups, snapshots, and replication) are now first-class pages.
|
||||
</p>
|
||||
</div>
|
||||
<For each={WHATS_NEW_FEATURE_CARDS}>
|
||||
{(card) => (
|
||||
<div class={`rounded-md border p-3 sm:p-4 ${card.accent}`}>
|
||||
<div class="flex items-center gap-2 text-sm font-semibold text-inherit">
|
||||
<WhatsNewFeatureIcon card={card} />
|
||||
{card.title}
|
||||
</div>
|
||||
<p class="mt-1.5 sm:mt-2 text-xs text-inherit">{card.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-sky-200 bg-sky-50 p-3 sm:p-4 dark:border-sky-800 dark:bg-sky-900/40">
|
||||
<div class="flex items-center gap-2 text-sm font-medium text-sky-900 dark:text-sky-100">
|
||||
<ChartBarIcon class="h-4 w-4 flex-shrink-0" />
|
||||
Anonymous telemetry
|
||||
{WHATS_NEW_TELEMETRY_TITLE}
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-sky-900 dark:text-sky-200">{WHATS_NEW_TELEMETRY_COPY[0]}</p>
|
||||
<p class="mt-1.5 text-xs text-sky-900 dark:text-sky-200">
|
||||
Pulse now sends a lightweight anonymous ping once a day — just a random install ID,
|
||||
version, platform, resource counts, and feature flags. No hostnames, credentials, IP
|
||||
addresses, or personal information is ever sent.
|
||||
</p>
|
||||
<p class="mt-1.5 text-xs text-sky-900 dark:text-sky-200">
|
||||
This helps the developer understand how Pulse is used and prioritise what to build
|
||||
next. You can disable it any time in{' '}
|
||||
<span class="font-medium">Settings → System → General</span> or by setting{' '}
|
||||
{WHATS_NEW_TELEMETRY_COPY[1]} You can disable it any time in{' '}
|
||||
<span class="font-medium">{WHATS_NEW_TELEMETRY_SETTINGS_PATH}</span> or by setting{' '}
|
||||
<code class="rounded bg-sky-100 px-1 py-0.5 text-[10px] font-mono dark:bg-sky-800">
|
||||
PULSE_TELEMETRY=false
|
||||
{WHATS_NEW_TELEMETRY_ENV_VAR}
|
||||
</code>
|
||||
.{' '}
|
||||
<a
|
||||
href="https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md"
|
||||
href={WHATS_NEW_PRIVACY_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="underline hover:text-sky-700 dark:hover:text-sky-100"
|
||||
>
|
||||
Full details
|
||||
{WHATS_NEW_TELEMETRY_PRIVACY_LABEL}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -132,38 +111,38 @@ export function WhatsNewModal() {
|
|||
<label class="flex items-center gap-2 text-sm text-muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={dontShowAgain()}
|
||||
onChange={(event) => setDontShowAgain(event.currentTarget.checked)}
|
||||
checked={state.dontShowAgain()}
|
||||
onChange={(event) => state.setDontShowAgain(event.currentTarget.checked)}
|
||||
class="h-4 w-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500 focus:ring-2"
|
||||
/>
|
||||
Don't show again
|
||||
{WHATS_NEW_DO_NOT_SHOW_LABEL}
|
||||
</label>
|
||||
|
||||
<a
|
||||
href={DOCS_URL}
|
||||
href={WHATS_NEW_DOCS_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
Documentation
|
||||
{WHATS_NEW_DOCS_LABEL}
|
||||
<ExternalLinkIcon class="h-4 w-4" />
|
||||
</a>
|
||||
<a
|
||||
href="/recovery?view=events&mode=remote"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
>
|
||||
Recovery events
|
||||
{WHATS_NEW_RECOVERY_LINK_LABEL}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0 flex items-center justify-end border-t border-border bg-surface-hover px-4 sm:px-6 py-3 sm:py-4">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
onClick={state.handleClose}
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700"
|
||||
type="button"
|
||||
>
|
||||
Let's go
|
||||
{WHATS_NEW_PRIMARY_ACTION_LABEL}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
|
||||
import { WhatsNewModal } from '@/components/shared/WhatsNewModal';
|
||||
import whatsNewModalSource from '@/components/shared/WhatsNewModal.tsx?raw';
|
||||
import whatsNewModalModelSource from '@/components/shared/whatsNewModalModel.ts?raw';
|
||||
import whatsNewModalStateSource from '@/components/shared/useWhatsNewModalState.ts?raw';
|
||||
import { STORAGE_KEYS } from '@/utils/localStorage';
|
||||
|
||||
describe('WhatsNewModal', () => {
|
||||
|
|
@ -12,6 +15,30 @@ describe('WhatsNewModal', () => {
|
|||
cleanup();
|
||||
});
|
||||
|
||||
it('keeps whats new modal on shell, runtime, and model owners', () => {
|
||||
expect(whatsNewModalSource).toContain('useWhatsNewModalState');
|
||||
expect(whatsNewModalSource).toContain('WHATS_NEW_FEATURE_CARDS');
|
||||
expect(whatsNewModalSource).not.toContain('createLocalStorageBooleanSignal');
|
||||
expect(whatsNewModalSource).not.toContain('createSignal');
|
||||
expect(whatsNewModalSource).not.toContain('WHATS_NEW_NAV_V2_SHOWN');
|
||||
expect(whatsNewModalSource).not.toContain('Infrastructure');
|
||||
expect(whatsNewModalSource).not.toContain('Documentation');
|
||||
expect(whatsNewModalSource).not.toContain('https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md');
|
||||
|
||||
expect(whatsNewModalStateSource).toContain('export function useWhatsNewModalState');
|
||||
expect(whatsNewModalStateSource).toContain('createLocalStorageBooleanSignal');
|
||||
expect(whatsNewModalStateSource).toContain('createSignal');
|
||||
expect(whatsNewModalStateSource).toContain('STORAGE_KEYS.WHATS_NEW_NAV_V2_SHOWN');
|
||||
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('WHATS_NEW_DOCS_LABEL');
|
||||
expect(whatsNewModalModelSource).toContain("title: 'Infrastructure'");
|
||||
});
|
||||
|
||||
it('renders when the navigation modal has not been seen yet', async () => {
|
||||
render(() => <WhatsNewModal />);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
import { createSignal } from 'solid-js';
|
||||
import { createLocalStorageBooleanSignal, STORAGE_KEYS } from '@/utils/localStorage';
|
||||
|
||||
export function useWhatsNewModalState() {
|
||||
const [hasSeen, setHasSeen] = createLocalStorageBooleanSignal(
|
||||
STORAGE_KEYS.WHATS_NEW_NAV_V2_SHOWN,
|
||||
false,
|
||||
);
|
||||
const [dontShowAgain, setDontShowAgain] = createSignal(true);
|
||||
const [dismissedForSession, setDismissedForSession] = createSignal(false);
|
||||
|
||||
const isOpen = () => !hasSeen() && !dismissedForSession();
|
||||
|
||||
const handleClose = () => {
|
||||
if (dontShowAgain()) {
|
||||
setHasSeen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setDismissedForSession(true);
|
||||
};
|
||||
|
||||
return {
|
||||
dontShowAgain,
|
||||
handleClose,
|
||||
isOpen,
|
||||
setDontShowAgain,
|
||||
};
|
||||
}
|
||||
55
frontend-modern/src/components/shared/whatsNewModalModel.ts
Normal file
55
frontend-modern/src/components/shared/whatsNewModalModel.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
export interface WhatsNewFeatureCard {
|
||||
accent: string;
|
||||
description: string;
|
||||
icon: 'infrastructure' | 'workloads' | 'storage' | 'recovery';
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const WHATS_NEW_DOCS_URL = 'https://github.com/rcourtman/Pulse/blob/main/docs/README.md';
|
||||
export const WHATS_NEW_PRIVACY_URL =
|
||||
'https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md';
|
||||
|
||||
export const WHATS_NEW_FEATURE_CARDS: WhatsNewFeatureCard[] = [
|
||||
{
|
||||
accent: 'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900',
|
||||
description: 'Proxmox nodes, agents, and container runtimes live together in one unified view.',
|
||||
icon: 'infrastructure',
|
||||
title: 'Infrastructure',
|
||||
},
|
||||
{
|
||||
accent: 'border-purple-200 bg-purple-50 dark:border-purple-800 dark:bg-purple-900',
|
||||
description: 'All VMs, containers, and Kubernetes workloads now share a single list.',
|
||||
icon: 'workloads',
|
||||
title: 'Workloads',
|
||||
},
|
||||
{
|
||||
accent: 'border-emerald-200 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-900',
|
||||
description: 'Storage is now a top-level destination across all systems.',
|
||||
icon: 'storage',
|
||||
title: 'Storage',
|
||||
},
|
||||
{
|
||||
accent: 'border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-900',
|
||||
description:
|
||||
'Recovery events (backups, snapshots, and replication) are now first-class pages.',
|
||||
icon: 'recovery',
|
||||
title: 'Recovery',
|
||||
},
|
||||
];
|
||||
|
||||
export const WHATS_NEW_TITLE = 'Welcome to the New Navigation!';
|
||||
export const WHATS_NEW_SUBTITLE =
|
||||
'Everything is now organized by what you want to do, not where the data comes from.';
|
||||
export const WHATS_NEW_TELEMETRY_TITLE = 'Anonymous telemetry';
|
||||
export const WHATS_NEW_TELEMETRY_COPY = [
|
||||
'Pulse now sends a lightweight anonymous ping once a day — just a random install ID, version, platform, resource counts, and feature flags. No hostnames, credentials, IP addresses, or personal information is ever sent.',
|
||||
'This helps the developer understand how Pulse is used and prioritise what to build next.',
|
||||
];
|
||||
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_PRIMARY_ACTION_LABEL = "Let's go";
|
||||
export const WHATS_NEW_CLOSE_LABEL = 'Close';
|
||||
export const WHATS_NEW_DOCS_LABEL = 'Documentation';
|
||||
export const WHATS_NEW_RECOVERY_LINK_LABEL = 'Recovery events';
|
||||
export const WHATS_NEW_DO_NOT_SHOW_LABEL = "Don't show again";
|
||||
|
|
@ -30,6 +30,8 @@ import mobileNavBarModelSource from '@/components/shared/mobileNavBarModel.ts?ra
|
|||
import infrastructureSelectorSource from '@/components/shared/InfrastructureSelector.tsx?raw';
|
||||
import pulseDataGridSource from '@/components/shared/PulseDataGrid.tsx?raw';
|
||||
import pulseDataGridModelSource from '@/components/shared/pulseDataGridModel.ts?raw';
|
||||
import whatsNewModalSource from '@/components/shared/WhatsNewModal.tsx?raw';
|
||||
import whatsNewModalModelSource from '@/components/shared/whatsNewModalModel.ts?raw';
|
||||
import searchFieldSource from '@/components/shared/SearchField.tsx?raw';
|
||||
import searchFieldModelSource from '@/components/shared/searchFieldModel.ts?raw';
|
||||
import searchInputSource from '@/components/shared/SearchInput.tsx?raw';
|
||||
|
|
@ -53,6 +55,7 @@ import historyChartStateSource from '@/components/shared/useHistoryChartState.ts
|
|||
import mobileNavBarStateSource from '@/components/shared/useMobileNavBarState.ts?raw';
|
||||
import infrastructureSelectorStateSource from '@/components/shared/useInfrastructureSelectorState.ts?raw';
|
||||
import pulseDataGridStateSource from '@/components/shared/usePulseDataGridState.ts?raw';
|
||||
import whatsNewModalStateSource from '@/components/shared/useWhatsNewModalState.ts?raw';
|
||||
import searchFieldStateSource from '@/components/shared/useSearchFieldState.ts?raw';
|
||||
import searchInputStateSource from '@/components/shared/useSearchInputState.ts?raw';
|
||||
import searchTipsPopoverStateSource from '@/components/shared/useSearchTipsPopoverState.ts?raw';
|
||||
|
|
@ -2706,6 +2709,24 @@ describe('frontend resource type boundaries', () => {
|
|||
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverPositionClass');
|
||||
expect(searchTipsPopoverModelSource).toContain('getSearchTipsPopoverTriggerVariant');
|
||||
expect(searchTipsPopoverModelSource).toContain('shouldSearchTipsPopoverOpenOnHover');
|
||||
expect(whatsNewModalSource).toContain('useWhatsNewModalState');
|
||||
expect(whatsNewModalSource).toContain('WHATS_NEW_FEATURE_CARDS');
|
||||
expect(whatsNewModalSource).not.toContain('createLocalStorageBooleanSignal');
|
||||
expect(whatsNewModalSource).not.toContain('createSignal');
|
||||
expect(whatsNewModalSource).not.toContain('WHATS_NEW_NAV_V2_SHOWN');
|
||||
expect(whatsNewModalSource).not.toContain('Documentation');
|
||||
expect(whatsNewModalSource).not.toContain(
|
||||
'https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md',
|
||||
);
|
||||
expect(whatsNewModalStateSource).toContain('createLocalStorageBooleanSignal');
|
||||
expect(whatsNewModalStateSource).toContain('createSignal');
|
||||
expect(whatsNewModalStateSource).toContain('STORAGE_KEYS.WHATS_NEW_NAV_V2_SHOWN');
|
||||
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('WHATS_NEW_DOCS_LABEL');
|
||||
expect(tooltipSource).toContain('useTooltipState');
|
||||
expect(tooltipSource).toContain('createTooltipSystemState');
|
||||
expect(tooltipSource).not.toContain('createSignal');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue