mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-25 04:33:03 +00:00
Split monitored system limit warning banner owners
This commit is contained in:
@@ -597,6 +597,15 @@ That banner boundary now also owns the canonical monitored-system naming
|
||||
surface directly: the shared warning component path and exported symbol are
|
||||
`MonitoredSystemLimitWarningBanner`, and future work may not reintroduce an
|
||||
agent-era banner filename or component name as the primary primitive.
|
||||
That shared monitored-system warning banner now also follows the shell/runtime/model
|
||||
owner split. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx`
|
||||
stays the render shell, `frontend-modern/src/components/shared/useMonitoredSystemLimitWarningBannerState.ts`
|
||||
owns entitlement load, warning metric emission, migration/upgrade click tracking,
|
||||
and upgrade-link runtime, and
|
||||
`frontend-modern/src/components/shared/monitoredSystemLimitWarningBannerModel.ts`
|
||||
owns monitored-system summary, migration copy, overflow summary, and tone/text-class
|
||||
policy. Future warning-banner work should extend those owners instead of pushing
|
||||
entitlement orchestration, tracking, or naming math back into the shared shell.
|
||||
First-session educational surfaces must also stay brief, flat, and model-led.
|
||||
When Pulse needs to teach a user how a flow works, the primary on-screen
|
||||
guidance should collapse to a few short descriptions of the real product
|
||||
|
||||
@@ -1,143 +1,55 @@
|
||||
import { Component, Show, createEffect, createMemo } from 'solid-js';
|
||||
import { Component, Show } from 'solid-js';
|
||||
import {
|
||||
entitlements,
|
||||
getLimit,
|
||||
getUpgradeActionUrlOrFallback,
|
||||
hasMigrationGap,
|
||||
legacyConnections,
|
||||
} from '@/stores/license';
|
||||
import {
|
||||
trackUpgradeClicked,
|
||||
trackUpgradeMetricEvent,
|
||||
UPGRADE_METRIC_EVENTS,
|
||||
} from '@/utils/upgradeMetrics';
|
||||
MONITORED_SYSTEM_LIMIT_BILLING_HREF,
|
||||
MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_HREF,
|
||||
MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_LABEL,
|
||||
MONITORED_SYSTEM_LIMIT_LEARN_MORE_LABEL,
|
||||
MONITORED_SYSTEM_LIMIT_UPGRADE_LABEL,
|
||||
} from './monitoredSystemLimitWarningBannerModel';
|
||||
import { useMonitoredSystemLimitWarningBannerState } from './useMonitoredSystemLimitWarningBannerState';
|
||||
|
||||
export const MonitoredSystemLimitWarningBanner: Component = () => {
|
||||
// No onMount load — TrialBanner (mounted above) already calls loadLicenseStatus().
|
||||
|
||||
const monitoredSystemLimit = createMemo(() => getLimit('max_monitored_systems'));
|
||||
|
||||
const isUrgent = createMemo(() => {
|
||||
const state = monitoredSystemLimit()?.state;
|
||||
return state === 'warning' || state === 'enforced';
|
||||
});
|
||||
|
||||
const migrationGap = createMemo(() => hasMigrationGap());
|
||||
const migrationCounts = createMemo(() => legacyConnections());
|
||||
const legacyConnectionTotal = createMemo(() => {
|
||||
const counts = migrationCounts();
|
||||
return counts.proxmox_nodes + counts.docker_hosts + counts.kubernetes_clusters;
|
||||
});
|
||||
const showBanner = createMemo(() => Boolean(monitoredSystemLimit()) && isUrgent());
|
||||
const monitoredSystemSummary = createMemo(() => {
|
||||
const limit = monitoredSystemLimit();
|
||||
if (!limit) return '';
|
||||
return `Monitored systems: ${limit.current}/${limit.limit}`;
|
||||
});
|
||||
const legacyBreakdown = createMemo(() => {
|
||||
const counts = migrationCounts();
|
||||
const parts: string[] = [];
|
||||
if (counts.proxmox_nodes > 0) {
|
||||
parts.push(
|
||||
`${counts.proxmox_nodes} Proxmox ${counts.proxmox_nodes === 1 ? 'node' : 'nodes'}`,
|
||||
);
|
||||
}
|
||||
if (counts.docker_hosts > 0) {
|
||||
parts.push(`${counts.docker_hosts} Docker ${counts.docker_hosts === 1 ? 'host' : 'hosts'}`);
|
||||
}
|
||||
if (counts.kubernetes_clusters > 0) {
|
||||
parts.push(
|
||||
`${counts.kubernetes_clusters} Kubernetes ${counts.kubernetes_clusters === 1 ? 'cluster' : 'clusters'}`,
|
||||
);
|
||||
}
|
||||
return parts.join(', ');
|
||||
});
|
||||
const migrationMessage = createMemo(() => {
|
||||
const total = legacyConnectionTotal();
|
||||
if (total <= 0) return '';
|
||||
const noun = total === 1 ? 'resource' : 'resources';
|
||||
const breakdown = legacyBreakdown();
|
||||
return `You also have ${total} ${noun} connected via API or legacy collectors${breakdown ? ` (${breakdown})` : ''} that count once toward your monitored-system cap when the same top-level system is discovered canonically.`;
|
||||
});
|
||||
const overflowDaysRemaining = createMemo(() => entitlements()?.overflow_days_remaining);
|
||||
const overflowSummary = createMemo(() => {
|
||||
const days = overflowDaysRemaining();
|
||||
if (!days) return '';
|
||||
return `Includes 1 temporary onboarding slot (${days}d remaining)`;
|
||||
});
|
||||
|
||||
// Emit limit_warning_shown once when the banner transitions to warning/enforced.
|
||||
// Uses previous-state tracking to avoid re-emitting on every reactive update.
|
||||
let prevUrgent = false;
|
||||
createEffect(() => {
|
||||
const urgent = isUrgent();
|
||||
const limit = monitoredSystemLimit();
|
||||
if (urgent && !prevUrgent && limit) {
|
||||
trackUpgradeMetricEvent({
|
||||
type: UPGRADE_METRIC_EVENTS.LIMIT_WARNING_SHOWN,
|
||||
surface: 'monitored_system_limit_banner',
|
||||
limit_key: 'max_monitored_systems',
|
||||
current_value: limit.current,
|
||||
limit_value: limit.limit,
|
||||
});
|
||||
}
|
||||
prevUrgent = !!urgent;
|
||||
});
|
||||
const state = useMonitoredSystemLimitWarningBannerState();
|
||||
|
||||
return (
|
||||
<Show when={showBanner()}>
|
||||
<Show when={state.showBanner()}>
|
||||
<div
|
||||
class={`mb-2 rounded-md border px-3 py-2 text-sm ${
|
||||
isUrgent()
|
||||
? 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-900 dark:bg-amber-900 dark:text-amber-100'
|
||||
: 'border-sky-200 bg-sky-50 text-sky-950 dark:border-sky-900 dark:bg-sky-950 dark:text-sky-100'
|
||||
}`}
|
||||
class={`mb-2 rounded-md border px-3 py-2 text-sm ${state.toneClass()}`}
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||
<span class={isUrgent() ? 'font-medium' : ''}>{monitoredSystemSummary()}</span>
|
||||
<Show when={overflowSummary()}>
|
||||
<span class="text-xs opacity-80">{overflowSummary()}</span>
|
||||
<span class={state.isUrgent() ? 'font-medium' : ''}>{state.monitoredSystemSummary()}</span>
|
||||
<Show when={state.overflowSummary()}>
|
||||
<span class="text-xs opacity-80">{state.overflowSummary()}</span>
|
||||
</Show>
|
||||
<Show when={migrationGap()}>
|
||||
<span
|
||||
class={`text-xs ${isUrgent() ? 'text-amber-800 dark:text-amber-200' : 'text-sky-800 dark:text-sky-200'}`}
|
||||
>
|
||||
{migrationMessage()}
|
||||
</span>
|
||||
<Show when={state.migrationGap()}>
|
||||
<span class={`text-xs ${state.migrationTextClass()}`}>{state.migrationMessage()}</span>
|
||||
</Show>
|
||||
<a
|
||||
class="text-xs font-medium underline underline-offset-2 hover:opacity-90"
|
||||
href="/settings/system/billing"
|
||||
href={MONITORED_SYSTEM_LIMIT_BILLING_HREF}
|
||||
>
|
||||
Learn more
|
||||
{MONITORED_SYSTEM_LIMIT_LEARN_MORE_LABEL}
|
||||
</a>
|
||||
<Show when={migrationGap()}>
|
||||
<Show when={state.migrationGap()}>
|
||||
<a
|
||||
class="text-xs font-medium underline underline-offset-2 hover:opacity-90"
|
||||
href="/settings"
|
||||
onClick={() =>
|
||||
trackUpgradeClicked(
|
||||
'monitored_system_limit_banner_install_v6_collectors',
|
||||
'max_monitored_systems',
|
||||
)
|
||||
}
|
||||
href={MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_HREF}
|
||||
onClick={state.handleInstallCollectorsClick}
|
||||
>
|
||||
Install v6 collectors
|
||||
{MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_LABEL}
|
||||
</a>
|
||||
</Show>
|
||||
<Show when={isUrgent()}>
|
||||
<Show when={state.isUrgent()}>
|
||||
<a
|
||||
class="text-xs font-semibold underline underline-offset-2 hover:opacity-90"
|
||||
href={getUpgradeActionUrlOrFallback('max_monitored_systems')}
|
||||
href={state.upgradeHref()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={() =>
|
||||
trackUpgradeClicked('monitored_system_limit_banner_upgrade', 'max_monitored_systems')
|
||||
}
|
||||
onClick={state.handleUpgradeClick}
|
||||
>
|
||||
Upgrade to add more
|
||||
{MONITORED_SYSTEM_LIMIT_UPGRADE_LABEL}
|
||||
</a>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -47,6 +47,7 @@ import infrastructureSelectorModelSource from '@/components/shared/infrastructur
|
||||
import infrastructureSummaryTableModelSource from '@/components/shared/infrastructureSummaryTableModel.ts?raw';
|
||||
import infrastructureSummaryTableStateSource from '@/components/shared/useInfrastructureSummaryTableState.ts?raw';
|
||||
import monitoredSystemLimitWarningBannerSource from '@/components/shared/MonitoredSystemLimitWarningBanner.tsx?raw';
|
||||
import monitoredSystemLimitWarningBannerModelSource from '@/components/shared/monitoredSystemLimitWarningBannerModel.ts?raw';
|
||||
import selectionCardGroupSource from '@/components/shared/SelectionCardGroup.tsx?raw';
|
||||
import tagBadgesSource from '@/components/shared/TagBadges.tsx?raw';
|
||||
import commandPaletteStateSource from '@/components/shared/useCommandPaletteState.ts?raw';
|
||||
@@ -68,6 +69,7 @@ import searchTipsPopoverStateSource from '@/components/shared/useSearchTipsPopov
|
||||
import tooltipStateSource from '@/components/shared/useTooltipState.ts?raw';
|
||||
import trialBannerStateSource from '@/components/shared/useTrialBannerState.ts?raw';
|
||||
import interactiveSparklineStateSource from '@/components/shared/useInteractiveSparklineState.ts?raw';
|
||||
import monitoredSystemLimitWarningBannerStateSource from '@/components/shared/useMonitoredSystemLimitWarningBannerState.ts?raw';
|
||||
import webInterfaceUrlFieldSource from '@/components/shared/WebInterfaceUrlField.tsx?raw';
|
||||
import webInterfaceUrlFieldModelSource from '@/components/shared/webInterfaceUrlFieldModel.ts?raw';
|
||||
import webInterfaceUrlFieldStateSource from '@/components/shared/useWebInterfaceUrlFieldState.ts?raw';
|
||||
@@ -218,18 +220,55 @@ describe('shared primitive guardrails', () => {
|
||||
});
|
||||
|
||||
it('keeps shared fleet limit banner copy on the monitored-system commercial term', () => {
|
||||
expect(monitoredSystemLimitWarningBannerSource).toContain('Monitored systems:');
|
||||
expect(monitoredSystemLimitWarningBannerSource).toContain('monitored-system cap');
|
||||
expect(monitoredSystemLimitWarningBannerSource).toContain('Install v6 collectors');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('v6 Unified Agents:');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain(
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain('Monitored systems:');
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain('monitored-system cap');
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain('Install v6 collectors');
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).not.toContain('v6 Unified Agents:');
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).not.toContain(
|
||||
'do not count toward Unified Agents.',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain(
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).not.toContain(
|
||||
'Install v6 Unified Agents',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps monitored system limit warning banner on shell, runtime, and model owners', () => {
|
||||
expect(monitoredSystemLimitWarningBannerSource).toContain(
|
||||
'useMonitoredSystemLimitWarningBannerState',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerSource).toContain(
|
||||
'MONITORED_SYSTEM_LIMIT_LEARN_MORE_LABEL',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('createEffect');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('createMemo');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('loadLicenseStatus');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('trackUpgradeMetricEvent');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('legacyConnections()');
|
||||
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain(
|
||||
'export function useMonitoredSystemLimitWarningBannerState',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('createEffect');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('createMemo');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('loadLicenseStatus');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('trackUpgradeMetricEvent');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('legacyConnections');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('handleUpgradeClick');
|
||||
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain(
|
||||
'getMonitoredSystemMigrationMessage',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain(
|
||||
'getMonitoredSystemBannerToneClass',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain(
|
||||
'MONITORED_SYSTEM_LIMIT_UPGRADE_LABEL',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain(
|
||||
'MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_LABEL',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps shared tag badges in the shared primitive boundary', () => {
|
||||
expect(tagBadgesSource).toContain("from '@/components/shared/Tooltip'");
|
||||
expect(guestRowSource).toContain("from '@/components/shared/TagBadges'");
|
||||
|
||||
+45
@@ -1,5 +1,8 @@
|
||||
import { cleanup, render, screen } from '@solidjs/testing-library';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import monitoredSystemLimitWarningBannerSource from '@/components/shared/MonitoredSystemLimitWarningBanner.tsx?raw';
|
||||
import monitoredSystemLimitWarningBannerModelSource from '@/components/shared/monitoredSystemLimitWarningBannerModel.ts?raw';
|
||||
import monitoredSystemLimitWarningBannerStateSource from '@/components/shared/useMonitoredSystemLimitWarningBannerState.ts?raw';
|
||||
|
||||
type MockEntitlements = {
|
||||
overflow_days_remaining?: number;
|
||||
@@ -26,6 +29,7 @@ const mockLegacyConnections = vi.hoisted(() =>
|
||||
);
|
||||
const mockTrackUpgradeMetricEvent = vi.hoisted(() => vi.fn());
|
||||
const mockTrackUpgradeClicked = vi.hoisted(() => vi.fn());
|
||||
const mockLoadLicenseStatus = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@/stores/license', () => ({
|
||||
entitlements: mockEntitlements,
|
||||
@@ -33,6 +37,7 @@ vi.mock('@/stores/license', () => ({
|
||||
getUpgradeActionUrlOrFallback: vi.fn(() => '/pricing?feature=max_monitored_systems'),
|
||||
hasMigrationGap: mockHasMigrationGap,
|
||||
legacyConnections: mockLegacyConnections,
|
||||
loadLicenseStatus: (...args: unknown[]) => mockLoadLicenseStatus(...args),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/upgradeMetrics', () => ({
|
||||
@@ -54,6 +59,8 @@ describe('MonitoredSystemLimitWarningBanner', () => {
|
||||
docker_hosts: 0,
|
||||
kubernetes_clusters: 0,
|
||||
});
|
||||
mockLoadLicenseStatus.mockReset();
|
||||
mockLoadLicenseStatus.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -61,10 +68,48 @@ describe('MonitoredSystemLimitWarningBanner', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('keeps monitored system limit warning banner on shell, runtime, and model owners', () => {
|
||||
expect(monitoredSystemLimitWarningBannerSource).toContain(
|
||||
'useMonitoredSystemLimitWarningBannerState',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerSource).toContain(
|
||||
'MONITORED_SYSTEM_LIMIT_LEARN_MORE_LABEL',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('createEffect');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('createMemo');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('loadLicenseStatus');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('trackUpgradeMetricEvent');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('legacyConnections()');
|
||||
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain(
|
||||
'export function useMonitoredSystemLimitWarningBannerState',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('createEffect');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('createMemo');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('loadLicenseStatus');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('trackUpgradeMetricEvent');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('legacyConnections');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('handleUpgradeClick');
|
||||
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain(
|
||||
'getMonitoredSystemMigrationMessage',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain(
|
||||
'getMonitoredSystemBannerToneClass',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain(
|
||||
'MONITORED_SYSTEM_LIMIT_UPGRADE_LABEL',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain(
|
||||
'MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_LABEL',
|
||||
);
|
||||
});
|
||||
|
||||
it('stays hidden for non-urgent pure v6 installs', async () => {
|
||||
const mod = await import('../MonitoredSystemLimitWarningBanner');
|
||||
render(() => <mod.MonitoredSystemLimitWarningBanner />);
|
||||
|
||||
expect(mockLoadLicenseStatus).toHaveBeenCalled();
|
||||
expect(screen.queryByText(/Monitored systems:/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
type LimitState = {
|
||||
current: number;
|
||||
limit: number;
|
||||
state?: string;
|
||||
};
|
||||
|
||||
type LegacyConnectionCounts = {
|
||||
proxmox_nodes: number;
|
||||
docker_hosts: number;
|
||||
kubernetes_clusters: number;
|
||||
};
|
||||
|
||||
export const MONITORED_SYSTEM_LIMIT_KEY = 'max_monitored_systems';
|
||||
export const MONITORED_SYSTEM_LIMIT_BILLING_HREF = '/settings/system/billing';
|
||||
export const MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_HREF = '/settings';
|
||||
export const MONITORED_SYSTEM_LIMIT_LEARN_MORE_LABEL = 'Learn more';
|
||||
export const MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_LABEL = 'Install v6 collectors';
|
||||
export const MONITORED_SYSTEM_LIMIT_UPGRADE_LABEL = 'Upgrade to add more';
|
||||
|
||||
export function isMonitoredSystemLimitUrgent(limit: LimitState | undefined): boolean {
|
||||
const state = limit?.state;
|
||||
return state === 'warning' || state === 'enforced';
|
||||
}
|
||||
|
||||
export function shouldShowMonitoredSystemLimitBanner(limit: LimitState | undefined): boolean {
|
||||
return Boolean(limit) && isMonitoredSystemLimitUrgent(limit);
|
||||
}
|
||||
|
||||
export function getMonitoredSystemSummary(limit: LimitState | undefined): string {
|
||||
if (!limit) return '';
|
||||
return `Monitored systems: ${limit.current}/${limit.limit}`;
|
||||
}
|
||||
|
||||
export function getMonitoredSystemLegacyConnectionTotal(counts: LegacyConnectionCounts): number {
|
||||
return counts.proxmox_nodes + counts.docker_hosts + counts.kubernetes_clusters;
|
||||
}
|
||||
|
||||
export function getMonitoredSystemLegacyBreakdown(counts: LegacyConnectionCounts): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (counts.proxmox_nodes > 0) {
|
||||
parts.push(
|
||||
`${counts.proxmox_nodes} Proxmox ${counts.proxmox_nodes === 1 ? 'node' : 'nodes'}`,
|
||||
);
|
||||
}
|
||||
if (counts.docker_hosts > 0) {
|
||||
parts.push(`${counts.docker_hosts} Docker ${counts.docker_hosts === 1 ? 'host' : 'hosts'}`);
|
||||
}
|
||||
if (counts.kubernetes_clusters > 0) {
|
||||
parts.push(
|
||||
`${counts.kubernetes_clusters} Kubernetes ${
|
||||
counts.kubernetes_clusters === 1 ? 'cluster' : 'clusters'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
export function getMonitoredSystemMigrationMessage(counts: LegacyConnectionCounts): string {
|
||||
const total = getMonitoredSystemLegacyConnectionTotal(counts);
|
||||
if (total <= 0) return '';
|
||||
|
||||
const noun = total === 1 ? 'resource' : 'resources';
|
||||
const breakdown = getMonitoredSystemLegacyBreakdown(counts);
|
||||
return `You also have ${total} ${noun} connected via API or legacy collectors${
|
||||
breakdown ? ` (${breakdown})` : ''
|
||||
} that count once toward your monitored-system cap when the same top-level system is discovered canonically.`;
|
||||
}
|
||||
|
||||
export function getMonitoredSystemOverflowSummary(daysRemaining: number | undefined): string {
|
||||
if (!daysRemaining) return '';
|
||||
return `Includes 1 temporary onboarding slot (${daysRemaining}d remaining)`;
|
||||
}
|
||||
|
||||
export function getMonitoredSystemBannerToneClass(isUrgent: boolean): string {
|
||||
return isUrgent
|
||||
? 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-900 dark:bg-amber-900 dark:text-amber-100'
|
||||
: 'border-sky-200 bg-sky-50 text-sky-950 dark:border-sky-900 dark:bg-sky-950 dark:text-sky-100';
|
||||
}
|
||||
|
||||
export function getMonitoredSystemMigrationTextClass(isUrgent: boolean): string {
|
||||
return isUrgent
|
||||
? 'text-amber-800 dark:text-amber-200'
|
||||
: 'text-sky-800 dark:text-sky-200';
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { createEffect, createMemo, onMount } from 'solid-js';
|
||||
import {
|
||||
entitlements,
|
||||
getLimit,
|
||||
getUpgradeActionUrlOrFallback,
|
||||
hasMigrationGap,
|
||||
legacyConnections,
|
||||
loadLicenseStatus,
|
||||
} from '@/stores/license';
|
||||
import {
|
||||
trackUpgradeClicked,
|
||||
trackUpgradeMetricEvent,
|
||||
UPGRADE_METRIC_EVENTS,
|
||||
} from '@/utils/upgradeMetrics';
|
||||
import {
|
||||
getMonitoredSystemBannerToneClass,
|
||||
getMonitoredSystemMigrationMessage,
|
||||
getMonitoredSystemMigrationTextClass,
|
||||
getMonitoredSystemOverflowSummary,
|
||||
getMonitoredSystemSummary,
|
||||
isMonitoredSystemLimitUrgent,
|
||||
MONITORED_SYSTEM_LIMIT_KEY,
|
||||
shouldShowMonitoredSystemLimitBanner,
|
||||
} from './monitoredSystemLimitWarningBannerModel';
|
||||
|
||||
export function useMonitoredSystemLimitWarningBannerState() {
|
||||
onMount(() => {
|
||||
void loadLicenseStatus();
|
||||
});
|
||||
|
||||
const monitoredSystemLimit = createMemo(() => getLimit(MONITORED_SYSTEM_LIMIT_KEY));
|
||||
const isUrgent = createMemo(() => isMonitoredSystemLimitUrgent(monitoredSystemLimit()));
|
||||
const showBanner = createMemo(() => shouldShowMonitoredSystemLimitBanner(monitoredSystemLimit()));
|
||||
const migrationGap = createMemo(() => hasMigrationGap());
|
||||
const migrationCounts = createMemo(() => legacyConnections());
|
||||
const monitoredSystemSummary = createMemo(() =>
|
||||
getMonitoredSystemSummary(monitoredSystemLimit()),
|
||||
);
|
||||
const migrationMessage = createMemo(() =>
|
||||
getMonitoredSystemMigrationMessage(migrationCounts()),
|
||||
);
|
||||
const overflowSummary = createMemo(() =>
|
||||
getMonitoredSystemOverflowSummary(entitlements()?.overflow_days_remaining),
|
||||
);
|
||||
const toneClass = createMemo(() => getMonitoredSystemBannerToneClass(isUrgent()));
|
||||
const migrationTextClass = createMemo(() =>
|
||||
getMonitoredSystemMigrationTextClass(isUrgent()),
|
||||
);
|
||||
const upgradeHref = createMemo(() =>
|
||||
getUpgradeActionUrlOrFallback(MONITORED_SYSTEM_LIMIT_KEY),
|
||||
);
|
||||
|
||||
let wasUrgent = false;
|
||||
createEffect(() => {
|
||||
const urgent = isUrgent();
|
||||
const limit = monitoredSystemLimit();
|
||||
if (urgent && !wasUrgent && limit) {
|
||||
trackUpgradeMetricEvent({
|
||||
type: UPGRADE_METRIC_EVENTS.LIMIT_WARNING_SHOWN,
|
||||
surface: 'monitored_system_limit_banner',
|
||||
limit_key: MONITORED_SYSTEM_LIMIT_KEY,
|
||||
current_value: limit.current,
|
||||
limit_value: limit.limit,
|
||||
});
|
||||
}
|
||||
wasUrgent = urgent;
|
||||
});
|
||||
|
||||
const handleInstallCollectorsClick = () => {
|
||||
trackUpgradeClicked(
|
||||
'monitored_system_limit_banner_install_v6_collectors',
|
||||
MONITORED_SYSTEM_LIMIT_KEY,
|
||||
);
|
||||
};
|
||||
|
||||
const handleUpgradeClick = () => {
|
||||
trackUpgradeClicked('monitored_system_limit_banner_upgrade', MONITORED_SYSTEM_LIMIT_KEY);
|
||||
};
|
||||
|
||||
return {
|
||||
handleInstallCollectorsClick,
|
||||
handleUpgradeClick,
|
||||
isUrgent,
|
||||
migrationGap,
|
||||
migrationMessage,
|
||||
migrationTextClass,
|
||||
monitoredSystemSummary,
|
||||
overflowSummary,
|
||||
showBanner,
|
||||
toneClass,
|
||||
upgradeHref,
|
||||
};
|
||||
}
|
||||
@@ -44,6 +44,8 @@ import tooltipSource from '@/components/shared/Tooltip.tsx?raw';
|
||||
import tooltipModelSource from '@/components/shared/tooltipModel.ts?raw';
|
||||
import trialBannerSource from '@/components/shared/TrialBanner.tsx?raw';
|
||||
import trialBannerModelSource from '@/components/shared/trialBannerModel.ts?raw';
|
||||
import monitoredSystemLimitWarningBannerSource from '@/components/shared/MonitoredSystemLimitWarningBanner.tsx?raw';
|
||||
import monitoredSystemLimitWarningBannerModelSource from '@/components/shared/monitoredSystemLimitWarningBannerModel.ts?raw';
|
||||
import infrastructureSummaryTableSource from '@/components/shared/InfrastructureSummaryTable.tsx?raw';
|
||||
import infrastructureSummaryTableRowSource from '@/components/shared/InfrastructureSummaryTableRow.tsx?raw';
|
||||
import interactiveSparklineSource from '@/components/shared/InteractiveSparkline.tsx?raw';
|
||||
@@ -67,6 +69,7 @@ import searchTipsPopoverStateSource from '@/components/shared/useSearchTipsPopov
|
||||
import tooltipStateSource from '@/components/shared/useTooltipState.ts?raw';
|
||||
import trialBannerStateSource from '@/components/shared/useTrialBannerState.ts?raw';
|
||||
import interactiveSparklineStateSource from '@/components/shared/useInteractiveSparklineState.ts?raw';
|
||||
import monitoredSystemLimitWarningBannerStateSource from '@/components/shared/useMonitoredSystemLimitWarningBannerState.ts?raw';
|
||||
import infrastructureSummaryTableStateSource from '@/components/shared/useInfrastructureSummaryTableState.ts?raw';
|
||||
import resourceBadgePresentationSource from '@/utils/resourceBadgePresentation.ts?raw';
|
||||
import workloadTypeBadgesSource from '@/components/shared/workloadTypeBadges.ts?raw';
|
||||
@@ -2751,6 +2754,35 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(trialBannerModelSource).toContain('getTrialBannerToneClass');
|
||||
expect(trialBannerModelSource).toContain('getTrialBannerStatusLabel');
|
||||
expect(trialBannerModelSource).toContain('TRIAL_BANNER_UPGRADE_LABEL');
|
||||
expect(monitoredSystemLimitWarningBannerSource).toContain(
|
||||
'useMonitoredSystemLimitWarningBannerState',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerSource).toContain(
|
||||
'MONITORED_SYSTEM_LIMIT_LEARN_MORE_LABEL',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('createEffect');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('createMemo');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('loadLicenseStatus');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('trackUpgradeMetricEvent');
|
||||
expect(monitoredSystemLimitWarningBannerSource).not.toContain('legacyConnections()');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('createEffect');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('createMemo');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('loadLicenseStatus');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('trackUpgradeMetricEvent');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('legacyConnections');
|
||||
expect(monitoredSystemLimitWarningBannerStateSource).toContain('handleUpgradeClick');
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain(
|
||||
'getMonitoredSystemMigrationMessage',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain(
|
||||
'getMonitoredSystemBannerToneClass',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain(
|
||||
'MONITORED_SYSTEM_LIMIT_UPGRADE_LABEL',
|
||||
);
|
||||
expect(monitoredSystemLimitWarningBannerModelSource).toContain(
|
||||
'MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_LABEL',
|
||||
);
|
||||
expect(whatsNewModalSource).toContain('useWhatsNewModalState');
|
||||
expect(whatsNewModalSource).toContain('WHATS_NEW_FEATURE_CARDS');
|
||||
expect(whatsNewModalSource).not.toContain('createLocalStorageBooleanSignal');
|
||||
|
||||
Reference in New Issue
Block a user