mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Fix missing utility navigation icons
This commit is contained in:
parent
0721669067
commit
beb143aade
6 changed files with 185 additions and 157 deletions
|
|
@ -1,12 +1,4 @@
|
|||
import {
|
||||
For,
|
||||
Show,
|
||||
Suspense,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
} from 'solid-js';
|
||||
import { For, Show, Suspense, createEffect, createMemo, createSignal, onCleanup } from 'solid-js';
|
||||
import type { JSX } from 'solid-js';
|
||||
import { useLocation, useNavigate } from '@solidjs/router';
|
||||
import BoxesIcon from 'lucide-solid/icons/boxes';
|
||||
|
|
@ -18,7 +10,11 @@ import BellIcon from 'lucide-solid/icons/bell';
|
|||
import SettingsIcon from 'lucide-solid/icons/settings';
|
||||
import Maximize2Icon from 'lucide-solid/icons/maximize-2';
|
||||
import Minimize2Icon from 'lucide-solid/icons/minimize-2';
|
||||
import { MobileNavBar } from '@/components/shared/MobileNavBar';
|
||||
import {
|
||||
MobileNavBar,
|
||||
type MobileNavBarPlatformTab as PlatformTab,
|
||||
type MobileNavBarUtilityTab as UtilityTab,
|
||||
} from '@/components/shared/MobileNavBar';
|
||||
import { dialogStackHasBlockingDialog } from '@/components/shared/useDialogState';
|
||||
import { OrgSwitcher } from '@/components/OrgSwitcher';
|
||||
import { PulsePatrolLogo } from '@/components/Brand/PulsePatrolLogo';
|
||||
|
|
@ -42,38 +38,12 @@ import { updateStore } from '@/stores/updates';
|
|||
import { aiChatStore } from '@/stores/aiChat';
|
||||
import { isPro } from '@/stores/licenseCommercial';
|
||||
import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy';
|
||||
import {
|
||||
AI_CHAT_LAUNCHER_ARIA_LABEL,
|
||||
getAIChatLauncherTitle,
|
||||
} from '@/utils/aiChatPresentation';
|
||||
import { AI_CHAT_LAUNCHER_ARIA_LABEL, getAIChatLauncherTitle } from '@/utils/aiChatPresentation';
|
||||
import type { AppConnectionStatus } from '@/useAppRuntimeState';
|
||||
|
||||
const ROOT_INFRASTRUCTURE_PATH = buildInfrastructurePath();
|
||||
const ROOT_WORKLOADS_PATH = buildWorkloadsPath();
|
||||
|
||||
type PlatformTab = {
|
||||
id: string;
|
||||
label: string;
|
||||
route: string;
|
||||
settingsRoute: string;
|
||||
tooltip: string;
|
||||
enabled: boolean;
|
||||
live: boolean;
|
||||
icon: JSX.Element;
|
||||
alwaysShow: boolean;
|
||||
badge?: string;
|
||||
};
|
||||
|
||||
type UtilityTab = {
|
||||
id: 'alerts' | 'ai' | 'settings';
|
||||
label: string;
|
||||
route: string;
|
||||
tooltip: string;
|
||||
badge: 'update' | 'pro' | null;
|
||||
count: number | undefined;
|
||||
breakdown: { warning: number; critical: number } | undefined;
|
||||
icon: JSX.Element;
|
||||
};
|
||||
const NAV_TAB_ICON_CLASS = 'w-4 h-4 shrink-0';
|
||||
|
||||
export interface AppLayoutProps {
|
||||
connectionStatus: () => AppConnectionStatus;
|
||||
|
|
@ -283,7 +253,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
const getActiveTabDesktop = () => getActiveTabForPath(location.pathname);
|
||||
const getActiveTabMobile = () => getActiveTabForPath(location.pathname);
|
||||
|
||||
const platformTabsDesktop = createMemo(() => {
|
||||
const platformTabs = createMemo<PlatformTab[]>(() => {
|
||||
const allPlatforms: PlatformTab[] = [
|
||||
{
|
||||
id: 'dashboard',
|
||||
|
|
@ -293,7 +263,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
tooltip: 'Environment overview and command center',
|
||||
enabled: true,
|
||||
live: true,
|
||||
icon: <LayoutDashboardIcon class="w-4 h-4 shrink-0" />,
|
||||
icon: LayoutDashboardIcon,
|
||||
alwaysShow: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -304,7 +274,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
tooltip: 'All agents and nodes across platforms',
|
||||
enabled: true,
|
||||
live: true,
|
||||
icon: <ServerIcon class="w-4 h-4 shrink-0" />,
|
||||
icon: ServerIcon,
|
||||
alwaysShow: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -315,69 +285,14 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
tooltip: 'VMs, containers, and Kubernetes workloads',
|
||||
enabled: true,
|
||||
live: true,
|
||||
icon: <BoxesIcon class="w-4 h-4 shrink-0" />,
|
||||
icon: BoxesIcon,
|
||||
alwaysShow: true,
|
||||
},
|
||||
...buildStorageRecoveryTabSpecs().map((tab) => ({
|
||||
...tab,
|
||||
enabled: true,
|
||||
live: true,
|
||||
icon: tab.id.startsWith('storage') ? (
|
||||
<HardDriveIcon class="w-4 h-4 shrink-0" />
|
||||
) : (
|
||||
<ArchiveIcon class="w-4 h-4 shrink-0" />
|
||||
),
|
||||
alwaysShow: true,
|
||||
})),
|
||||
];
|
||||
|
||||
return allPlatforms.filter((platform) => platform.alwaysShow || platform.enabled);
|
||||
});
|
||||
|
||||
const platformTabsMobile = createMemo(() => {
|
||||
const allPlatforms: PlatformTab[] = [
|
||||
{
|
||||
id: 'dashboard',
|
||||
label: 'Dashboard',
|
||||
route: DASHBOARD_PATH,
|
||||
settingsRoute: '/settings',
|
||||
tooltip: 'Environment overview and command center',
|
||||
enabled: true,
|
||||
live: true,
|
||||
icon: <LayoutDashboardIcon class="w-4 h-4 shrink-0" />,
|
||||
alwaysShow: true,
|
||||
},
|
||||
{
|
||||
id: 'infrastructure',
|
||||
label: 'Infrastructure',
|
||||
route: ROOT_INFRASTRUCTURE_PATH,
|
||||
settingsRoute: '/settings',
|
||||
tooltip: 'All agents and nodes across platforms',
|
||||
enabled: true,
|
||||
live: true,
|
||||
icon: <ServerIcon class="w-4 h-4 shrink-0" />,
|
||||
alwaysShow: true,
|
||||
},
|
||||
{
|
||||
id: 'workloads',
|
||||
label: 'Workloads',
|
||||
route: ROOT_WORKLOADS_PATH,
|
||||
settingsRoute: '/settings/workloads/docker',
|
||||
tooltip: 'VMs, containers, and Kubernetes workloads',
|
||||
enabled: true,
|
||||
live: true,
|
||||
icon: <BoxesIcon class="w-4 h-4 shrink-0" />,
|
||||
alwaysShow: true,
|
||||
},
|
||||
...buildStorageRecoveryTabSpecs().map((tab) => ({
|
||||
...tab,
|
||||
enabled: true,
|
||||
live: true,
|
||||
icon: tab.id.startsWith('storage') ? (
|
||||
<HardDriveIcon class="w-4 h-4 shrink-0" />
|
||||
) : (
|
||||
<ArchiveIcon class="w-4 h-4 shrink-0" />
|
||||
),
|
||||
icon: tab.id.startsWith('storage') ? HardDriveIcon : ArchiveIcon,
|
||||
alwaysShow: true,
|
||||
})),
|
||||
];
|
||||
|
|
@ -415,7 +330,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
badge: null,
|
||||
count: activeAlertCount,
|
||||
breakdown,
|
||||
icon: <BellIcon class="w-4 h-4 shrink-0" />,
|
||||
icon: BellIcon,
|
||||
},
|
||||
{
|
||||
id: 'ai',
|
||||
|
|
@ -425,7 +340,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
badge: null,
|
||||
count: undefined,
|
||||
breakdown: undefined,
|
||||
icon: <PulsePatrolLogo class="w-4 h-4 shrink-0" />,
|
||||
icon: PulsePatrolLogo,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -438,7 +353,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
badge: updateStore.isUpdateVisible() ? 'update' : null,
|
||||
count: undefined,
|
||||
breakdown: undefined,
|
||||
icon: <SettingsIcon class="w-4 h-4 shrink-0" />,
|
||||
icon: SettingsIcon,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -557,14 +472,24 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
class={`pulse-logo ${props.connectionStatus().kind === 'connected' && props.dataUpdated() ? 'animate-pulse-logo' : ''}`}
|
||||
>
|
||||
<title>Pulse Logo</title>
|
||||
<circle class="pulse-bg fill-blue-600 dark:fill-blue-500" cx="128" cy="128" r="122" />
|
||||
<circle
|
||||
class="pulse-bg fill-blue-600 dark:fill-blue-500"
|
||||
cx="128"
|
||||
cy="128"
|
||||
r="122"
|
||||
/>
|
||||
<circle
|
||||
class="pulse-ring fill-none stroke-white stroke-[14] opacity-[0.92]"
|
||||
cx="128"
|
||||
cy="128"
|
||||
r="84"
|
||||
/>
|
||||
<circle class="pulse-center fill-white dark:fill-[#dbeafe]" cx="128" cy="128" r="26" />
|
||||
<circle
|
||||
class="pulse-center fill-white dark:fill-[#dbeafe]"
|
||||
cx="128"
|
||||
cy="128"
|
||||
r="26"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-lg font-medium text-base-content">Pulse</span>
|
||||
<Show when={props.versionInfo()?.channel === 'rc'}>
|
||||
|
|
@ -604,10 +529,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
aria-label={kioskMode() ? 'Exit kiosk mode' : 'Enter kiosk mode'}
|
||||
aria-pressed={kioskMode()}
|
||||
>
|
||||
<Show
|
||||
when={kioskMode()}
|
||||
fallback={<Maximize2Icon class="h-4 w-4 flex-shrink-0" />}
|
||||
>
|
||||
<Show when={kioskMode()} fallback={<Maximize2Icon class="h-4 w-4 flex-shrink-0" />}>
|
||||
<Minimize2Icon class="h-4 w-4 flex-shrink-0" />
|
||||
</Show>
|
||||
</button>
|
||||
|
|
@ -638,10 +560,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
<ConnectionStatusBadge
|
||||
connectionStatus={props.connectionStatus}
|
||||
class="flex-shrink-0"
|
||||
/>
|
||||
<ConnectionStatusBadge connectionStatus={props.connectionStatus} class="flex-shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -652,10 +571,11 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
aria-label="Primary navigation"
|
||||
>
|
||||
<div class="flex items-end gap-1" role="group" aria-label="Infrastructure">
|
||||
<For each={platformTabsDesktop()}>
|
||||
<For each={platformTabs()}>
|
||||
{(platform) => {
|
||||
const isActive = () => getActiveTabDesktop() === platform.id;
|
||||
const disabled = () => !platform.enabled;
|
||||
const Icon = platform.icon;
|
||||
const baseClasses =
|
||||
'tab relative px-1.5 sm:px-3 py-1.5 text-xs sm:text-sm font-medium flex items-center gap-1 sm:gap-1.5 rounded-t border border-transparent transition-colors whitespace-nowrap cursor-pointer';
|
||||
|
||||
|
|
@ -683,7 +603,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
onClick={() => handlePlatformClick(platform)}
|
||||
title={title()}
|
||||
>
|
||||
{platform.icon}
|
||||
<Icon class={NAV_TAB_ICON_CLASS} />
|
||||
<span class="hidden xs:inline-flex items-center gap-1">
|
||||
<span>{platform.label}</span>
|
||||
<Show when={platform.badge}>
|
||||
|
|
@ -703,6 +623,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
<For each={utilityTabs()}>
|
||||
{(tab) => {
|
||||
const isActive = () => getActiveTabDesktop() === tab.id;
|
||||
const Icon = tab.icon;
|
||||
const baseClasses =
|
||||
'tab relative px-1.5 sm:px-3 py-1.5 text-xs sm:text-sm font-medium flex items-center gap-1 sm:gap-1.5 rounded-t border border-transparent transition-colors whitespace-nowrap cursor-pointer';
|
||||
|
||||
|
|
@ -722,7 +643,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
onClick={() => handleUtilityClick(tab)}
|
||||
title={tab.tooltip}
|
||||
>
|
||||
{tab.icon}
|
||||
<Icon class={NAV_TAB_ICON_CLASS} />
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="hidden xs:inline">{tab.label}</span>
|
||||
<span class="xs:hidden">{tab.label.charAt(0)}</span>
|
||||
|
|
@ -757,9 +678,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
/>
|
||||
</span>
|
||||
</Show>
|
||||
<Show
|
||||
when={tab.badge === 'pro' && !presentationPolicyHidesUpgradePrompts()}
|
||||
>
|
||||
<Show when={tab.badge === 'pro' && !presentationPolicyHidesUpgradePrompts()}>
|
||||
<span class="ml-1.5 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-900 rounded">
|
||||
Pro
|
||||
</span>
|
||||
|
|
@ -787,7 +706,7 @@ export function AppLayout(props: AppLayoutProps) {
|
|||
<Show when={!kioskMode()}>
|
||||
<MobileNavBar
|
||||
activeTab={getActiveTabMobile}
|
||||
platformTabs={platformTabsMobile}
|
||||
platformTabs={platformTabs}
|
||||
utilityTabs={utilityTabs}
|
||||
onPlatformClick={handlePlatformClick}
|
||||
onUtilityClick={handleUtilityClick}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,18 @@ describe('App architecture', () => {
|
|||
expect(appLayoutSource).not.toContain("route: '/operations',");
|
||||
expect(appLayoutSource).not.toContain('props.connected()');
|
||||
expect(appLayoutSource).toContain('const utilityTabs = createMemo(() =>');
|
||||
expect(appLayoutSource).toContain(
|
||||
"type MobileNavBarPlatformTab as PlatformTab,\n type MobileNavBarUtilityTab as UtilityTab,",
|
||||
);
|
||||
expect(appLayoutSource).toContain("const NAV_TAB_ICON_CLASS = 'w-4 h-4 shrink-0';");
|
||||
expect(appLayoutSource).toContain('const platformTabs = createMemo<PlatformTab[]>(() =>');
|
||||
expect(appLayoutSource).toContain('const Icon = platform.icon;');
|
||||
expect(appLayoutSource).toContain('const Icon = tab.icon;');
|
||||
expect(appLayoutSource).toContain('<Icon class={NAV_TAB_ICON_CLASS} />');
|
||||
expect(appLayoutSource).not.toContain('type PlatformTab = {');
|
||||
expect(appLayoutSource).not.toContain('type UtilityTab = {');
|
||||
expect(appLayoutSource).not.toContain('const platformTabsDesktop = createMemo(() =>');
|
||||
expect(appLayoutSource).not.toContain('const platformTabsMobile = createMemo(() =>');
|
||||
expect(appLayoutSource).not.toContain("import { isMultiTenantEnabled } from '@/stores/license';");
|
||||
expect(appLayoutSource).not.toContain('loadCommercialPosture');
|
||||
expect(appLayoutSource).not.toContain('buildReleaseNotesUrl');
|
||||
|
|
|
|||
86
frontend-modern/src/__tests__/AppLayout.test.tsx
Normal file
86
frontend-modern/src/__tests__/AppLayout.test.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { cleanup, render, screen, within } from '@solidjs/testing-library';
|
||||
import { Route, Router } from '@solidjs/router';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import type { State } from '@/types/api';
|
||||
import { AppLayout } from '@/AppLayout';
|
||||
|
||||
HTMLElement.prototype.scrollIntoView = vi.fn();
|
||||
|
||||
describe('AppLayout navigation icons', () => {
|
||||
beforeEach(() => {
|
||||
window.history.replaceState({}, '', '/dashboard');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
const renderLayout = () =>
|
||||
render(() => (
|
||||
<Router>
|
||||
<Route
|
||||
path="/dashboard"
|
||||
component={() => (
|
||||
<AppLayout
|
||||
connectionStatus={() => ({
|
||||
kind: 'connected',
|
||||
label: 'Connected',
|
||||
detail: 'Backend and live data stream are connected.',
|
||||
tone: 'healthy',
|
||||
})}
|
||||
dataUpdated={() => false}
|
||||
lastUpdateText={() => ''}
|
||||
versionInfo={() =>
|
||||
({
|
||||
version: '6.0.0-rc.2',
|
||||
channel: 'rc',
|
||||
isDevelopment: false,
|
||||
isDocker: false,
|
||||
}) as never
|
||||
}
|
||||
hasAuth={() => true}
|
||||
needsAuth={() => false}
|
||||
proxyAuthInfo={() => null}
|
||||
handleLogout={() => {}}
|
||||
state={() =>
|
||||
({
|
||||
activeAlerts: [{ id: 'alert-1', level: 'warning', acknowledged: false }],
|
||||
}) as unknown as State
|
||||
}
|
||||
tokenScopes={() => ['settings:read']}
|
||||
organizations={() => []}
|
||||
activeOrgID={() => 'default'}
|
||||
orgsLoading={() => false}
|
||||
showOrgSwitcher={() => false}
|
||||
onSwitchOrg={() => {}}
|
||||
>
|
||||
<div>Dashboard body</div>
|
||||
</AppLayout>
|
||||
)}
|
||||
/>
|
||||
</Router>
|
||||
));
|
||||
|
||||
it('renders fresh utility icons for both desktop and mobile navigation trees', () => {
|
||||
const { container } = renderLayout();
|
||||
|
||||
const desktopNav = screen.getByRole('tablist', { name: 'Primary navigation' });
|
||||
const systemGroup = desktopNav.querySelector('[aria-label="System"]');
|
||||
expect(systemGroup).toBeTruthy();
|
||||
|
||||
const desktopTabs = within(systemGroup as HTMLElement).getAllByRole('tab');
|
||||
expect(desktopTabs).toHaveLength(3);
|
||||
desktopTabs.forEach((tab) => {
|
||||
expect(tab.querySelector('svg')).toBeTruthy();
|
||||
});
|
||||
|
||||
const mobileTablist = screen.getByRole('tablist', { name: 'Mobile navigation' });
|
||||
['alerts', 'ai', 'settings'].forEach((tabId) => {
|
||||
const button = mobileTablist.querySelector<HTMLElement>(`[data-tab-id="${tabId}"]`);
|
||||
expect(button).toBeTruthy();
|
||||
expect(button?.querySelector('svg')).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(container).toHaveTextContent('Dashboard body');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,9 +1,6 @@
|
|||
import { For, Show } from 'solid-js';
|
||||
import { type MobileNavBarProps, useMobileNavBarState } from './useMobileNavBarState';
|
||||
import {
|
||||
getMobileNavAlertBadgeCounts,
|
||||
getMobileNavTabButtonClass,
|
||||
} from './mobileNavBarModel';
|
||||
import { getMobileNavAlertBadgeCounts, getMobileNavTabButtonClass } from './mobileNavBarModel';
|
||||
|
||||
export type {
|
||||
MobileNavBarPlatformTab,
|
||||
|
|
@ -13,6 +10,7 @@ export type {
|
|||
|
||||
export function MobileNavBar(props: MobileNavBarProps) {
|
||||
const mobileNav = useMobileNavBarState(props);
|
||||
const tabIconClass = 'h-4 w-4 shrink-0';
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -25,36 +23,43 @@ export function MobileNavBar(props: MobileNavBarProps) {
|
|||
aria-label="Mobile navigation"
|
||||
>
|
||||
<For each={mobileNav.orderedPlatformTabs()}>
|
||||
{(platform) => (
|
||||
<button
|
||||
type="button"
|
||||
data-tab-id={platform.id}
|
||||
onClick={() => mobileNav.handlePlatformClick(platform)}
|
||||
title={platform.tooltip}
|
||||
class={getMobileNavTabButtonClass({
|
||||
active: props.activeTab() === platform.id,
|
||||
enabled: platform.enabled,
|
||||
})}
|
||||
>
|
||||
<span class="relative flex items-center justify-center">{platform.icon}</span>
|
||||
<span class="whitespace-nowrap">{platform.label}</span>
|
||||
<Show when={!platform.enabled}>
|
||||
<span class="rounded-full bg-amber-100 px-1.5 py-0.5 text-[9px] font-semibold text-amber-700 dark:bg-amber-900 dark:text-amber-200">
|
||||
Setup
|
||||
{(platform) => {
|
||||
const Icon = platform.icon;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
data-tab-id={platform.id}
|
||||
onClick={() => mobileNav.handlePlatformClick(platform)}
|
||||
title={platform.tooltip}
|
||||
class={getMobileNavTabButtonClass({
|
||||
active: props.activeTab() === platform.id,
|
||||
enabled: platform.enabled,
|
||||
})}
|
||||
>
|
||||
<span class="relative flex items-center justify-center">
|
||||
<Icon class={tabIconClass} />
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={platform.badge}>
|
||||
<span class="rounded-full bg-surface-hover px-1.5 py-0.5 text-[9px] font-semibold text-muted">
|
||||
{platform.badge}
|
||||
</span>
|
||||
</Show>
|
||||
</button>
|
||||
)}
|
||||
<span class="whitespace-nowrap">{platform.label}</span>
|
||||
<Show when={!platform.enabled}>
|
||||
<span class="rounded-full bg-amber-100 px-1.5 py-0.5 text-[9px] font-semibold text-amber-700 dark:bg-amber-900 dark:text-amber-200">
|
||||
Setup
|
||||
</span>
|
||||
</Show>
|
||||
<Show when={platform.badge}>
|
||||
<span class="rounded-full bg-surface-hover px-1.5 py-0.5 text-[9px] font-semibold text-muted">
|
||||
{platform.badge}
|
||||
</span>
|
||||
</Show>
|
||||
</button>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
|
||||
<For each={mobileNav.orderedUtilityTabs()}>
|
||||
{(tab) => {
|
||||
const alertBadges = () => getMobileNavAlertBadgeCounts(tab);
|
||||
const Icon = tab.icon;
|
||||
|
||||
return (
|
||||
<button
|
||||
|
|
@ -67,7 +72,7 @@ export function MobileNavBar(props: MobileNavBarProps) {
|
|||
})}
|
||||
>
|
||||
<span class="relative flex items-center justify-center">
|
||||
{tab.icon}
|
||||
<Icon class={tabIconClass} />
|
||||
<Show when={alertBadges()}>
|
||||
{(badges) => (
|
||||
<span class="absolute -right-2 -top-1 flex items-center gap-1">
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { Component } from 'solid-js';
|
||||
import mobileNavBarSource from '@/components/shared/MobileNavBar.tsx?raw';
|
||||
import mobileNavBarModelSource from '@/components/shared/mobileNavBarModel.ts?raw';
|
||||
import mobileNavBarStateSource from '@/components/shared/useMobileNavBarState.ts?raw';
|
||||
|
|
@ -11,6 +12,11 @@ window.requestAnimationFrame = ((callback: FrameRequestCallback) => {
|
|||
return 1;
|
||||
}) as typeof window.requestAnimationFrame;
|
||||
|
||||
const DashboardIcon: Component<{ class?: string }> = (props) => <span class={props.class}>DB</span>;
|
||||
const StorageIcon: Component<{ class?: string }> = (props) => <span class={props.class}>ST</span>;
|
||||
const AlertsIcon: Component<{ class?: string }> = (props) => <span class={props.class}>AL</span>;
|
||||
const SettingsIcon: Component<{ class?: string }> = (props) => <span class={props.class}>SE</span>;
|
||||
|
||||
describe('MobileNavBar', () => {
|
||||
it('keeps the mobile nav on shell, runtime, and model owners', () => {
|
||||
expect(mobileNavBarSource).toContain('useMobileNavBarState');
|
||||
|
|
@ -47,7 +53,7 @@ describe('MobileNavBar', () => {
|
|||
tooltip: 'Storage',
|
||||
enabled: true,
|
||||
live: true,
|
||||
icon: <span>ST</span>,
|
||||
icon: StorageIcon,
|
||||
alwaysShow: true,
|
||||
},
|
||||
{
|
||||
|
|
@ -58,7 +64,7 @@ describe('MobileNavBar', () => {
|
|||
tooltip: 'Dashboard',
|
||||
enabled: true,
|
||||
live: true,
|
||||
icon: <span>DB</span>,
|
||||
icon: DashboardIcon,
|
||||
alwaysShow: true,
|
||||
},
|
||||
]}
|
||||
|
|
@ -71,7 +77,7 @@ describe('MobileNavBar', () => {
|
|||
badge: 'pro',
|
||||
count: undefined,
|
||||
breakdown: undefined,
|
||||
icon: <span>SE</span>,
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
id: 'alerts',
|
||||
|
|
@ -81,7 +87,7 @@ describe('MobileNavBar', () => {
|
|||
badge: null,
|
||||
count: 5,
|
||||
breakdown: { critical: 2, warning: 3 },
|
||||
icon: <span>AL</span>,
|
||||
icon: AlertsIcon,
|
||||
},
|
||||
]}
|
||||
onPlatformClick={onPlatformClick}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import type { JSX } from 'solid-js';
|
||||
import type { Component } from 'solid-js';
|
||||
|
||||
export type MobileNavBarIcon = Component<{ class?: string }>;
|
||||
|
||||
export type MobileNavBarPlatformTab = {
|
||||
id: string;
|
||||
|
|
@ -8,7 +10,7 @@ export type MobileNavBarPlatformTab = {
|
|||
tooltip: string;
|
||||
enabled: boolean;
|
||||
live: boolean;
|
||||
icon: JSX.Element;
|
||||
icon: MobileNavBarIcon;
|
||||
alwaysShow: boolean;
|
||||
badge?: string;
|
||||
};
|
||||
|
|
@ -21,7 +23,7 @@ export type MobileNavBarUtilityTab = {
|
|||
badge: 'update' | 'pro' | null;
|
||||
count: number | undefined;
|
||||
breakdown: { warning: number; critical: number } | undefined;
|
||||
icon: JSX.Element;
|
||||
icon: MobileNavBarIcon;
|
||||
};
|
||||
|
||||
export type MobileNavBarProps = {
|
||||
|
|
@ -91,9 +93,7 @@ export function getMobileNavTabButtonClass(options: {
|
|||
enabled?: boolean;
|
||||
}): string {
|
||||
return `relative flex min-h-10 shrink-0 flex-col items-center gap-1 rounded-md px-2 py-1.5 text-[11px] font-medium transition-colors ${
|
||||
options.active
|
||||
? 'bg-blue-50 text-blue-700 dark:bg-blue-900 dark:text-blue-300'
|
||||
: 'text-muted'
|
||||
options.active ? 'bg-blue-50 text-blue-700 dark:bg-blue-900 dark:text-blue-300' : 'text-muted'
|
||||
} ${options.enabled === false ? 'opacity-70' : ''}`.trim();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue