Unify platform icons through a canonical helper

Platform icons were chosen independently in AppLayout, each platform page
surface, and the alerts thresholds table, so the thresholds page drifted
(Server/Boxes/Network/HardDrive) from the tabs and pages it should match.
Route every site through a single getPlatformIcon helper and complete the
brand-icon direction Proxmox already used: add inlined CC0 Docker,
Kubernetes, and TrueNAS marks. vSphere keeps a generic CPU glyph since
VMware has no legible square brand mark.
This commit is contained in:
rcourtman
2026-05-28 21:25:31 +01:00
parent 9a9b6c2b2c
commit f4ffdab695
12 changed files with 143 additions and 38 deletions
+7 -12
View File
@@ -6,12 +6,7 @@ import SettingsIcon from 'lucide-solid/icons/settings';
import Maximize2Icon from 'lucide-solid/icons/maximize-2';
import Minimize2Icon from 'lucide-solid/icons/minimize-2';
import SparklesIcon from 'lucide-solid/icons/sparkles';
import ContainerIcon from 'lucide-solid/icons/container';
import ServerIcon from 'lucide-solid/icons/server';
import ShipWheelIcon from 'lucide-solid/icons/ship-wheel';
import DatabaseIcon from 'lucide-solid/icons/database';
import CpuIcon from 'lucide-solid/icons/cpu';
import { ProxmoxIcon } from '@/components/icons/ProxmoxIcon';
import { getPlatformIcon } from '@/features/platformPage/platformIcon';
import {
MobileNavBar,
type MobileNavBarPrimaryTab as PrimaryTab,
@@ -338,7 +333,7 @@ export function AppLayout(props: AppLayoutProps) {
tooltip: 'Proxmox VE, Backup Server, Mail Gateway, storage, backups, and guests',
enabled: isVisible('proxmox'),
live: isVisible('proxmox'),
icon: ProxmoxIcon,
icon: getPlatformIcon('proxmox'),
alwaysShow: false,
},
{
@@ -349,7 +344,7 @@ export function AppLayout(props: AppLayoutProps) {
tooltip: 'Docker / Podman runtime lens: hosts, containers, and Swarm services',
enabled: isVisible('docker'),
live: isVisible('docker'),
icon: ContainerIcon,
icon: getPlatformIcon('docker'),
alwaysShow: false,
},
{
@@ -360,7 +355,7 @@ export function AppLayout(props: AppLayoutProps) {
tooltip: 'Kubernetes clusters, nodes, pods, deployments, and services',
enabled: isVisible('kubernetes'),
live: isVisible('kubernetes'),
icon: ShipWheelIcon,
icon: getPlatformIcon('kubernetes'),
alwaysShow: false,
},
{
@@ -371,7 +366,7 @@ export function AppLayout(props: AppLayoutProps) {
tooltip: 'TrueNAS hosts, storage, and apps',
enabled: isVisible('truenas'),
live: isVisible('truenas'),
icon: DatabaseIcon,
icon: getPlatformIcon('truenas'),
alwaysShow: false,
},
{
@@ -382,7 +377,7 @@ export function AppLayout(props: AppLayoutProps) {
tooltip: 'VMware vSphere hosts, virtual machines, datastores, and networks',
enabled: isVisible('vmware'),
live: isVisible('vmware'),
icon: CpuIcon,
icon: getPlatformIcon('vmware'),
alwaysShow: false,
},
{
@@ -393,7 +388,7 @@ export function AppLayout(props: AppLayoutProps) {
tooltip: 'Pulse Agent machines, agentless computers, and availability checks',
enabled: isVisible('standalone'),
live: isVisible('standalone'),
icon: ServerIcon,
icon: getPlatformIcon('standalone'),
alwaysShow: false,
},
];
@@ -259,7 +259,7 @@ describe('App architecture', () => {
expect(appLayoutSource).toContain('return `${count} ${tab.label}`;');
expect(appLayoutSource).toContain('const primaryTabs = createMemo<PrimaryTab[]>(() =>');
expect(appLayoutSource).toContain("id: 'proxmox',");
expect(appLayoutSource).toContain('icon: ProxmoxIcon,');
expect(appLayoutSource).toContain("icon: getPlatformIcon('proxmox'),");
expect(appLayoutSource).toContain('const Icon = tab.icon;');
expect(appLayoutSource).toContain('const Icon = tab.icon;');
expect(appLayoutSource).toContain('aria-label={tab.label}');
@@ -1,12 +1,5 @@
import { Show } from 'solid-js';
import Server from 'lucide-solid/icons/server';
import Mail from 'lucide-solid/icons/mail';
import Users from 'lucide-solid/icons/users';
import Boxes from 'lucide-solid/icons/boxes';
import Cpu from 'lucide-solid/icons/cpu';
import Database from 'lucide-solid/icons/database';
import HardDrive from 'lucide-solid/icons/hard-drive';
import Network from 'lucide-solid/icons/network';
import { getPlatformIcon } from '@/features/platformPage/platformIcon';
import { FilterBar, type FilterDef } from '@/components/shared/FilterBar';
import { useBreakpoint } from '@/hooks/useBreakpoint';
import { BulkEditDialog } from './BulkEditDialog';
@@ -36,14 +29,14 @@ export function ThresholdsTable(props: ThresholdsTableProps) {
setValue: (value) => state.handleTabClick(value as ThresholdsActiveTab),
defaultValue: 'proxmox',
options: () => [
{ value: 'proxmox', label: 'Proxmox', icon: Server },
{ value: 'docker', label: 'Docker', icon: Boxes },
{ value: 'kubernetes', label: 'Kubernetes', icon: Network },
{ value: 'truenas', label: 'TrueNAS', icon: HardDrive },
{ value: 'vmware', label: 'vSphere', icon: Cpu },
{ value: 'pbs', label: 'PBS', icon: Database },
{ value: 'pmg', label: 'PMG', icon: Mail },
{ value: 'systems', label: 'Systems', icon: Users },
{ value: 'proxmox', label: 'Proxmox', icon: getPlatformIcon('proxmox') },
{ value: 'docker', label: 'Docker', icon: getPlatformIcon('docker') },
{ value: 'kubernetes', label: 'Kubernetes', icon: getPlatformIcon('kubernetes') },
{ value: 'truenas', label: 'TrueNAS', icon: getPlatformIcon('truenas') },
{ value: 'vmware', label: 'vSphere', icon: getPlatformIcon('vmware') },
{ value: 'pbs', label: 'PBS', icon: getPlatformIcon('pbs') },
{ value: 'pmg', label: 'PMG', icon: getPlatformIcon('pmg') },
{ value: 'systems', label: 'Systems', icon: getPlatformIcon('systems') },
],
},
{
@@ -0,0 +1,23 @@
import type { Component } from 'solid-js';
interface DockerIconProps {
class?: string;
title?: string;
}
// Path data sourced from simple-icons (CC0); inlined to avoid pulling the full dependency.
const DOCKER_PATH =
'M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z';
export const DockerIcon: Component<DockerIconProps> = (props) => (
<svg
class={props.class}
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-label={props.title ?? 'Docker'}
>
<title>{props.title ?? 'Docker'}</title>
<path d={DOCKER_PATH} fill="currentColor" />
</svg>
);
@@ -0,0 +1,23 @@
import type { Component } from 'solid-js';
interface KubernetesIconProps {
class?: string;
title?: string;
}
// Path data sourced from simple-icons (CC0); inlined to avoid pulling the full dependency.
const KUBERNETES_PATH =
'M10.204 14.35l.007.01-.999 2.413a5.171 5.171 0 0 1-2.075-2.597l2.578-.437.004.005a.44.44 0 0 1 .484.606zm-.833-2.129a.44.44 0 0 0 .173-.756l.002-.011L7.585 9.7a5.143 5.143 0 0 0-.73 3.255l2.514-.725.002-.009zm1.145-1.98a.44.44 0 0 0 .699-.337l.01-.005.15-2.62a5.144 5.144 0 0 0-3.01 1.442l2.147 1.523.004-.002zm.76 2.75l.723.349.722-.347.18-.78-.5-.623h-.804l-.5.623.179.779zm1.5-3.095a.44.44 0 0 0 .7.336l.008.003 2.134-1.513a5.188 5.188 0 0 0-2.992-1.442l.148 2.615.002.001zm10.876 5.97l-5.773 7.181a1.6 1.6 0 0 1-1.248.594l-9.261.003a1.6 1.6 0 0 1-1.247-.596l-5.776-7.18a1.583 1.583 0 0 1-.307-1.34L2.1 5.573c.108-.47.425-.864.863-1.073L11.305.513a1.606 1.606 0 0 1 1.385 0l8.345 3.985c.438.209.755.604.863 1.073l2.062 8.955c.108.47-.005.963-.308 1.34zm-3.289-2.057c-.042-.01-.103-.026-.145-.034-.174-.033-.315-.025-.479-.038-.35-.037-.638-.067-.895-.148-.105-.04-.18-.165-.216-.216l-.201-.059a6.45 6.45 0 0 0-.105-2.332 6.465 6.465 0 0 0-.936-2.163c.052-.047.15-.133.177-.159.008-.09.001-.183.094-.282.197-.185.444-.338.743-.522.142-.084.273-.137.415-.242.032-.024.076-.062.11-.089.24-.191.295-.52.123-.736-.172-.216-.506-.236-.745-.045-.034.027-.08.062-.111.088-.134.116-.217.23-.33.35-.246.25-.45.458-.673.609-.097.056-.239.037-.303.033l-.19.135a6.545 6.545 0 0 0-4.146-2.003l-.012-.223c-.065-.062-.143-.115-.163-.25-.022-.268.015-.557.057-.905.023-.163.061-.298.068-.475.001-.04-.001-.099-.001-.142 0-.306-.224-.555-.5-.555-.275 0-.499.249-.499.555l.001.014c0 .041-.002.092 0 .128.006.177.044.312.067.475.042.348.078.637.056.906a.545.545 0 0 1-.162.258l-.012.211a6.424 6.424 0 0 0-4.166 2.003 8.373 8.373 0 0 1-.18-.128c-.09.012-.18.04-.297-.029-.223-.15-.427-.358-.673-.608-.113-.12-.195-.234-.329-.349-.03-.026-.077-.062-.111-.088a.594.594 0 0 0-.348-.132.481.481 0 0 0-.398.176c-.172.216-.117.546.123.737l.007.005.104.083c.142.105.272.159.414.242.299.185.546.338.743.522.076.082.09.226.1.288l.16.143a6.462 6.462 0 0 0-1.02 4.506l-.208.06c-.055.072-.133.184-.215.217-.257.081-.546.11-.895.147-.164.014-.305.006-.48.039-.037.007-.09.02-.133.03l-.004.002-.007.002c-.295.071-.484.342-.423.608.061.267.349.429.645.365l.007-.001.01-.003.129-.029c.17-.046.294-.113.448-.172.33-.118.604-.217.87-.256.112-.009.23.069.288.101l.217-.037a6.5 6.5 0 0 0 2.88 3.596l-.09.218c.033.084.069.199.044.282-.097.252-.263.517-.452.813-.091.136-.185.242-.268.399-.02.037-.045.095-.064.134-.128.275-.034.591.213.71.248.12.556-.007.69-.282v-.002c.02-.039.046-.09.062-.127.07-.162.094-.301.144-.458.132-.332.205-.68.387-.897.05-.06.13-.082.215-.105l.113-.205a6.453 6.453 0 0 0 4.609.012l.106.192c.086.028.18.042.256.155.136.232.229.507.342.84.05.156.074.295.145.457.016.037.043.09.062.129.133.276.442.402.69.282.247-.118.341-.435.213-.71-.02-.039-.045-.096-.065-.134-.083-.156-.177-.261-.268-.398-.19-.296-.346-.541-.443-.793-.04-.13.007-.21.038-.294-.018-.022-.059-.144-.083-.202a6.499 6.499 0 0 0 2.88-3.622c.064.01.176.03.213.038.075-.05.144-.114.28-.104.266.039.54.138.87.256.154.06.277.128.448.173.036.01.088.019.13.028l.009.003.007.001c.297.064.584-.098.645-.365.06-.266-.128-.537-.423-.608zM16.4 9.701l-1.95 1.746v.005a.44.44 0 0 0 .173.757l.003.01 2.526.728a5.199 5.199 0 0 0-.108-1.674A5.208 5.208 0 0 0 16.4 9.7zm-4.013 5.325a.437.437 0 0 0-.404-.232.44.44 0 0 0-.372.233h-.002l-1.268 2.292a5.164 5.164 0 0 0 3.326.003l-1.27-2.296h-.01zm1.888-1.293a.44.44 0 0 0-.27.036.44.44 0 0 0-.214.572l-.003.004 1.01 2.438a5.15 5.15 0 0 0 2.081-2.615l-2.6-.44-.004.005z';
export const KubernetesIcon: Component<KubernetesIconProps> = (props) => (
<svg
class={props.class}
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-label={props.title ?? 'Kubernetes'}
>
<title>{props.title ?? 'Kubernetes'}</title>
<path d={KUBERNETES_PATH} fill="currentColor" />
</svg>
);
@@ -0,0 +1,23 @@
import type { Component } from 'solid-js';
interface TrueNASIconProps {
class?: string;
title?: string;
}
// Path data sourced from simple-icons (CC0); inlined to avoid pulling the full dependency.
const TRUENAS_PATH =
'M24 10.049v5.114l-10.949 6.324v-5.114L24 10.049zm-24 0v5.114l10.956 6.324v-5.114L0 10.049zm12.004-.605l-4.433 2.559 4.433 2.559 4.429-2.559-4.429-2.559zm10.952-1.207l-9.905-5.723v5.118l5.473 3.164 4.432-2.559zm-12-.605V2.513L1.044 8.236l4.432 2.555 5.48-3.159z';
export const TrueNASIcon: Component<TrueNASIconProps> = (props) => (
<svg
class={props.class}
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
aria-label={props.title ?? 'TrueNAS'}
>
<title>{props.title ?? 'TrueNAS'}</title>
<path d={TRUENAS_PATH} fill="currentColor" />
</svg>
);
@@ -1,6 +1,6 @@
import { useLocation } from '@solidjs/router';
import ContainerIcon from 'lucide-solid/icons/container';
import { Show, createMemo, createSignal } from 'solid-js';
import { getPlatformIcon } from '@/features/platformPage/platformIcon';
import { useUnifiedResources } from '@/hooks/useUnifiedResources';
import {
PLATFORM_HEALTH_FILTER_OPTIONS,
@@ -37,7 +37,8 @@ import {
const DOCKER_RESOURCE_QUERY =
'type=agent,docker-host,app-container,docker-service,docker-image,docker-volume,docker-network,docker-task,docker-swarm-node,docker-secret,docker-config';
const dockerIcon = () => <ContainerIcon class="h-6 w-6 text-slate-400" />;
const DockerIcon = getPlatformIcon('docker');
const dockerIcon = () => <DockerIcon class="h-6 w-6 text-slate-400" />;
export function DockerPageSurface() {
const location = useLocation();
@@ -1,6 +1,6 @@
import { useLocation } from '@solidjs/router';
import ShipWheelIcon from 'lucide-solid/icons/ship-wheel';
import { Show, createMemo, createSignal, type Accessor } from 'solid-js';
import { getPlatformIcon } from '@/features/platformPage/platformIcon';
import { useUnifiedResources } from '@/hooks/useUnifiedResources';
import {
PLATFORM_HEALTH_FILTER_OPTIONS,
@@ -41,7 +41,8 @@ import {
const KUBERNETES_RESOURCE_QUERY =
'type=k8s-cluster,k8s-node,pod,k8s-deployment,k8s-replicaset,k8s-namespace,k8s-service,k8s-statefulset,k8s-daemonset,k8s-job,k8s-cronjob,k8s-ingress,k8s-endpoint-slice,k8s-network-policy,k8s-persistent-volume,k8s-persistent-volume-claim,k8s-storage-class,k8s-configmap,k8s-secret,k8s-serviceaccount,k8s-role,k8s-cluster-role,k8s-role-binding,k8s-cluster-role-binding,k8s-resource-quota,k8s-limit-range,k8s-pod-disruption-budget,k8s-horizontal-pod-autoscaler,k8s-event,agent';
const k8sIcon = () => <ShipWheelIcon class="h-6 w-6 text-slate-400" />;
const KubernetesIcon = getPlatformIcon('kubernetes');
const k8sIcon = () => <KubernetesIcon class="h-6 w-6 text-slate-400" />;
export function KubernetesPageSurface() {
const location = useLocation();
@@ -0,0 +1,43 @@
import type { JSX } from 'solid-js';
import CpuIcon from 'lucide-solid/icons/cpu';
import DatabaseIcon from 'lucide-solid/icons/database';
import MailIcon from 'lucide-solid/icons/mail';
import ServerIcon from 'lucide-solid/icons/server';
import UsersIcon from 'lucide-solid/icons/users';
import { DockerIcon } from '@/components/icons/DockerIcon';
import { KubernetesIcon } from '@/components/icons/KubernetesIcon';
import { ProxmoxIcon } from '@/components/icons/ProxmoxIcon';
import { TrueNASIcon } from '@/components/icons/TrueNASIcon';
export type PlatformIconKey =
| 'proxmox'
| 'docker'
| 'kubernetes'
| 'truenas'
| 'vmware'
| 'standalone'
| 'pbs'
| 'pmg'
| 'systems';
export type PlatformIcon = (props: { class?: string }) => JSX.Element;
// Single source of truth for the icon shown per platform. Brand marks (Proxmox,
// Docker, Kubernetes, TrueNAS) come from inlined simple-icons SVGs; vSphere has
// no legible square brand glyph so it keeps a generic CPU mark. PBS/PMG/Systems/
// Standalone are not third-party brands and use semantic generic icons.
const PLATFORM_ICONS: Record<PlatformIconKey, PlatformIcon> = {
proxmox: ProxmoxIcon,
docker: DockerIcon,
kubernetes: KubernetesIcon,
truenas: TrueNASIcon,
vmware: CpuIcon,
standalone: ServerIcon,
pbs: DatabaseIcon,
pmg: MailIcon,
systems: UsersIcon,
};
export function getPlatformIcon(key: PlatformIconKey): PlatformIcon {
return PLATFORM_ICONS[key];
}
@@ -14,7 +14,7 @@ import {
isWorkloadTableMetricHistoryRange,
type WorkloadTableMetricHistoryRange,
} from '@/components/Workloads/workloadMetricHistoryModel';
import { ProxmoxIcon } from '@/components/icons/ProxmoxIcon';
import { getPlatformIcon } from '@/features/platformPage/platformIcon';
import { usePersistentSignal } from '@/hooks/usePersistentSignal';
import { STORAGE_KEYS } from '@/utils/localStorage';
import { resourceMatchesSearch } from '@/utils/resourceSearchMatch';
@@ -58,6 +58,7 @@ const PROXMOX_WORKLOAD_STATUS_OPTIONS: readonly WorkloadsStatusOption[] = [
{ value: 'stopped', label: 'Stopped' },
];
const ProxmoxIcon = getPlatformIcon('proxmox');
const proxmoxIcon = () => <ProxmoxIcon class="h-6 w-6 text-slate-400" />;
export function ProxmoxPageSurface() {
@@ -1,6 +1,6 @@
import { useLocation } from '@solidjs/router';
import DatabaseIcon from 'lucide-solid/icons/database';
import { Show, createMemo, type Accessor } from 'solid-js';
import { getPlatformIcon } from '@/features/platformPage/platformIcon';
import { useRecoveryPoints } from '@/hooks/useRecoveryPoints';
import { useUnifiedResources } from '@/hooks/useUnifiedResources';
import {
@@ -36,7 +36,8 @@ const TRUENAS_RESOURCE_QUERY =
const TRUENAS_PLATFORM_FILTER = 'truenas';
const VALID_TABS = new Set<TrueNASPageTabId>(TRUENAS_TAB_SPECS.map((tab) => tab.id));
const truenasIcon = () => <DatabaseIcon class="h-6 w-6 text-slate-400" />;
const TrueNASIcon = getPlatformIcon('truenas');
const truenasIcon = () => <TrueNASIcon class="h-6 w-6 text-slate-400" />;
export function TrueNASPageSurface() {
const location = useLocation();
@@ -1,6 +1,6 @@
import { useLocation } from '@solidjs/router';
import CpuIcon from 'lucide-solid/icons/cpu';
import { Show, createMemo, createResource, type Accessor } from 'solid-js';
import { getPlatformIcon } from '@/features/platformPage/platformIcon';
import { ResourceAPI } from '@/api/resources';
import { useUnifiedResources } from '@/hooks/useUnifiedResources';
import {
@@ -61,7 +61,8 @@ const VMWARE_WORKLOAD_STATUS_OPTIONS: readonly WorkloadsStatusOption[] = [
{ value: 'stopped', label: 'Powered off' },
];
const vmwareIcon = () => <CpuIcon class="h-6 w-6 text-slate-400" />;
const VmwareIcon = getPlatformIcon('vmware');
const vmwareIcon = () => <VmwareIcon class="h-6 w-6 text-slate-400" />;
export function VmwarePageSurface() {
const location = useLocation();