From 3504764fe883e7ea21993694bea113b326c2cf5c Mon Sep 17 00:00:00 2001 From: Maya Date: Sat, 6 Sep 2025 17:58:06 -0400 Subject: [PATCH] services frontend getting there --- .../features/discovery/DiscoveryStatus.svelte | 12 - .../features/nodes/components/NodeCard.svelte | 17 +- .../NodeEditModal/Details/DetailsForm.svelte | 37 +- .../NodeEditModal/NodeEditor.svelte | 39 +- .../Services/ServicesConfigPanel.svelte | 595 ++++++++++-------- .../Services/ServicesForm.svelte | 293 ++++----- src/lib/features/nodes/store.ts | 32 +- src/lib/features/nodes/types/base.ts | 14 +- src/lib/features/services/types/base.ts | 53 ++ src/lib/shared/components/data/Tag.svelte | 9 +- .../components/forms/DynamicField.svelte | 263 -------- .../components/forms/JsonContainer.svelte | 107 ---- .../components/forms/ListConfigEditor.svelte | 2 + .../components/forms/ListManager.svelte | 2 + .../components/forms/ListSelectItem.svelte | 26 +- .../shared/components/forms/RichSelect.svelte | 84 ++- src/lib/shared/stores/registry.ts | 19 +- 17 files changed, 650 insertions(+), 954 deletions(-) create mode 100644 src/lib/features/services/types/base.ts delete mode 100644 src/lib/shared/components/forms/DynamicField.svelte delete mode 100644 src/lib/shared/components/forms/JsonContainer.svelte diff --git a/src/lib/features/discovery/DiscoveryStatus.svelte b/src/lib/features/discovery/DiscoveryStatus.svelte index f2786fdb..6d80daf6 100644 --- a/src/lib/features/discovery/DiscoveryStatus.svelte +++ b/src/lib/features/discovery/DiscoveryStatus.svelte @@ -7,7 +7,6 @@ import { getDaemonDiscoveryState } from '../daemons/store'; import RichSelect from '$lib/shared/components/forms/RichSelect.svelte'; import { getNodeTargetString } from "../nodes/store"; - import { nodeStatuses } from '$lib/shared/stores/registry'; import DaemonDiscoveryStatus from './DaemonDiscoveryStatus.svelte'; import { type TagProps } from '$lib/shared/components/data/types'; import { get } from 'svelte/store'; @@ -15,9 +14,6 @@ let selectedDaemonId: string | null = null; $: discoveryData = getDaemonDiscoveryState(selectedDaemonId, get(sessions)); $: selectedDaemon = $daemons.find(daemon => daemon.id == selectedDaemonId); - $: selectedNode = $nodes.find(node => node.id == selectedDaemon?.node_id); - $: nodeStyle = nodeStatuses.getColor(selectedNode?.status || null); - // Auto-select daemon logic: prioritize daemon with active session, fallback to first daemon $: if (!selectedDaemonId && $daemons.length > 0) { @@ -50,17 +46,9 @@ value: d.id, label: node?.name || `Daemon ${d.id.substring(0, 8)}`, description: node ? `on ${getNodeTargetString(node?.target)}` : `Daemon ${d.id.substring(0, 8)}`, - status: node?.status } })} getOptionId={(option) => option.value} - getOptionTags={(option): TagProps[] => { - return [{ - label: option.status, - bgColor: nodeStyle.bg, - textColor: nodeStyle.text - }] - }} onSelect={handleDaemonSelect} /> {/if} diff --git a/src/lib/features/nodes/components/NodeCard.svelte b/src/lib/features/nodes/components/NodeCard.svelte index 38a14184..d892503b 100644 --- a/src/lib/features/nodes/components/NodeCard.svelte +++ b/src/lib/features/nodes/components/NodeCard.svelte @@ -2,12 +2,12 @@ import { Edit, Radar, Trash2 } from 'lucide-svelte'; import { getNodeTargetString } from '../store'; import type { Node } from '../types/base'; - import { capabilities, nodeStatuses, nodeTypes } from '$lib/shared/stores/registry'; import GenericCard from '$lib/shared/components/data/GenericCard.svelte'; import type { Daemon } from '$lib/features/daemons/types/base'; import { getDaemonDiscoveryState } from '$lib/features/daemons/store'; import DaemonDiscoveryStatus from '$lib/features/discovery/DaemonDiscoveryStatus.svelte'; import { sessions } from '$lib/features/discovery/store'; + import { services } from '$lib/shared/stores/registry'; export let node: Node; export let daemon: Daemon | null; @@ -26,12 +26,8 @@ // Build card data $: cardData = { title: node.name, - subtitle: nodeTypes.getDisplay(node.node_type), - status: node.monitoring_interval == 0 ? 'Monitoring Disabled' : nodeStatuses.getDisplay(node.status || null), - statusColor: node.monitoring_interval == 0 ? 'text-gray-400' : nodeStatuses.getColor(node.status || null).text, - icon: nodeTypes.getIconComponent(node.node_type), iconColor: 'text-blue-400', - + icon: services.getIconComponent(node.services[0].type), sections: connectionInfo ? [{ label: 'Connection', value: connectionInfo @@ -39,12 +35,11 @@ lists: [ { - label: 'Capabilities', - items: node.capabilities.map(cap => { - const [capId, capInfo] = Object.entries(cap)[0]; + label: 'Services', + items: node.services.map(cap => { return ({ - id: capId, - label: capabilities.getDisplay(capId), + id: cap.type, + label: services.getDisplay(cap.type), bgColor: 'bg-purple-900/30', color: 'text-purple-300' }) diff --git a/src/lib/features/nodes/components/NodeEditModal/Details/DetailsForm.svelte b/src/lib/features/nodes/components/NodeEditModal/Details/DetailsForm.svelte index 9b024377..ca38d94f 100644 --- a/src/lib/features/nodes/components/NodeEditModal/Details/DetailsForm.svelte +++ b/src/lib/features/nodes/components/NodeEditModal/Details/DetailsForm.svelte @@ -5,33 +5,28 @@ import { required } from 'svelte-forms/validators'; import type { Node } from '$lib/features/nodes/types/base'; import { maxLength, validNodeType } from '$lib/shared/components/forms/validators'; - import { nodeTargets, nodeTypes } from '$lib/shared/stores/registry'; + import { nodeTargets } from '$lib/shared/stores/registry'; export let form: any; export let formData: Node; - export let isEditing: boolean = false; // Create form fields with validation const name = field('name', formData.name, [required(), maxLength(100)]); const description = field('description', formData.description || '', [maxLength(500)]); - const nodeType = field('node_type', formData.node_type, [validNodeType(isEditing)]); const hostname = field('hostname', formData.hostname || ''); // Add fields to form onMount(() => { form.name = name form.description = description - form.nodeType = nodeType form.hostname = hostname }); // Update formData when field values change $: formData.name = $name.value; $: formData.description = $description.value; - $: formData.node_type = $nodeType.value; $: formData.hostname = $hostname.value; - $: nodeTypeOptions = nodeTypes.getItems().map(t => {return {value:t.id, label: t.display_name}}); $: targetTypeOptions = nodeTargets.getItems().map(t => {return {value:t.id, label: t.display_name, description: t.description, icon: t.icon}}); // Initialize target if needed @@ -91,36 +86,6 @@

- -
- - - {#if $nodeType.errors.length > 0} -
- -

{$nodeType.errors[0]}

-
- {/if} -

- Classification for this node type -

-
-
@@ -204,12 +205,11 @@ - {:else if activeTab === 'capabilities'} + {:else if activeTab === 'services'}
{#if !isEditing}
@@ -221,10 +221,9 @@ {/if}
-
diff --git a/src/lib/features/nodes/components/NodeEditModal/Services/ServicesConfigPanel.svelte b/src/lib/features/nodes/components/NodeEditModal/Services/ServicesConfigPanel.svelte index 11001d6a..1589affb 100644 --- a/src/lib/features/nodes/components/NodeEditModal/Services/ServicesConfigPanel.svelte +++ b/src/lib/features/nodes/components/NodeEditModal/Services/ServicesConfigPanel.svelte @@ -1,326 +1,365 @@ -{#if !capability || !schema} -
-
-
No capability selected
-
Select a capability from the list to configure it
-
-
-{:else} - {@const config = getCapabilityConfig(capability)} -
- -
-
- {#if schema.capability_info.icon} - {@const iconStyle = createStyle(schema.capability_info.color, schema.capability_info.icon)} - - {/if} -

- {schema.capability_info.display_name} -

+{#if service && serviceMetadata} +
+ +
+
+

{serviceMetadata.display_name}

+ +
+

{serviceMetadata.description}

+
- {#if schema.capability_info.description} -

{schema.capability_info.description}

- {/if}
- - -
- - {#if !getCapabilityConfig(capability).system_assigned && capabilityNameField} + + +
+ + + {#if serviceNameField}
-
{/if} - - - {#if schema.capability_fields.length > 0} -
-

Configuration

-
- {#each schema.capability_fields as field} - capabilityConfig[field.id] = value} - /> - {/each} -
+ + + {#if confirmedField} +
+ +

+ Mark as confirmed if you've verified this service is actually running +

{/if} - - - +
+
+

Ports

+ +
+ + {#if service.ports.length === 0} +
+ No ports configured. Click "Add Port" to add one. +
+ {:else} +
+ {#each service.ports as port, index} +
+
+
+
Port Number
+ handlePortNumberChange(index, e)} + class="w-full px-2 py-1 text-sm bg-gray-700 border border-gray-600 rounded text-white focus:ring-2 focus:ring-blue-500" + />
- - - {#if isExpanded} -
-
- {#each section.test_fields as field} - updateTestConfig(sectionIndex, field.id, value)} - disabled={!testConfig?.enabled} - /> - {/each} -
+
+
Protocol
+
+ +
- {/if} +
+
+ +
- {/each} -
-
- {/if} --> - - - {#if schema.warnings.length > 0} -
-
Warnings
-
- {#each schema.warnings as warning} -

{warning.message}

- {/each} -
+
+ {/each}
{/if} - - {#if schema.errors.length > 0} -
-
Errors
-
- {#each schema.errors as error} -

{error.message}

- {/each} -
+
+ + +
+
+

Endpoints

+ +
+ + {#if service.endpoints.length === 0} +
+ No endpoints configured. Click "Add Endpoint" to add one. +
+ {:else} +
+ {#each service.endpoints as endpoint, index} +
+
+
+
Path
+ handleEndpointPathChange(index, e)} + placeholder="/api/health" + class="w-full px-2 py-1 text-sm bg-gray-700 border border-gray-600 rounded text-white focus:ring-2 focus:ring-blue-500" + /> +
+
+ +
+
+
+ {/each}
{/if}
+{:else} +
+
+
No service selected
+
Select a service from the list to configure it
+
+
{/if} \ No newline at end of file diff --git a/src/lib/features/nodes/components/NodeEditModal/Services/ServicesForm.svelte b/src/lib/features/nodes/components/NodeEditModal/Services/ServicesForm.svelte index 5b5ea041..b754dcea 100644 --- a/src/lib/features/nodes/components/NodeEditModal/Services/ServicesForm.svelte +++ b/src/lib/features/nodes/components/NodeEditModal/Services/ServicesForm.svelte @@ -1,183 +1,186 @@ !getCapabilityConfig(selected).system_assigned} - emptyMessage="No capabilities configured. Add one to get started." + allowReorder={true} + placeholder="Select service type to add..." {getOptionId} {getOptionLabel} {getOptionDescription} {getOptionIcon} {getOptionIconColor} + {getOptionTags} + {getOptionCategory} {getItemId} {getItemLabel} {getItemDescription} {getItemIcon} {getItemIconColor} + {getItemTags} - onAdd={handleAddCapability} - onChange={handleCapabilityChange} + onAdd={handleAddService} + onRemove={handleRemoveService} + onChange={handleServiceChange} > - onChange(updatedCapability)} + service={selectedItem} + onChange={(updatedService) => onChange(updatedService)} /> \ No newline at end of file diff --git a/src/lib/features/nodes/store.ts b/src/lib/features/nodes/store.ts index 1a80d612..bbad213a 100644 --- a/src/lib/features/nodes/store.ts +++ b/src/lib/features/nodes/store.ts @@ -1,11 +1,9 @@ import { writable } from 'svelte/store'; import type { Node } from "./types/base"; import { api } from '../../shared/utils/api'; -import { createPoller, type Poller } from '../../shared/utils/polling'; import type { NodeTarget } from './types/targets'; -import { pushError, pushInfo, pushWarning } from '$lib/shared/stores/feedback'; +import { pushInfo, pushWarning } from '$lib/shared/stores/feedback'; import { utcTimeZoneSentinel, uuidv4Sentinel } from '$lib/shared/utils/formatting'; -import { testTypes } from '$lib/shared/stores/registry'; export const nodes = writable([]); export const polling = writable(false); @@ -57,14 +55,14 @@ export async function createNode(data: Node) { interface UpdateNodeResponse { node: Node, - capability_test_changes: Record, + // capability_test_changes: Record, subnet_changes: NodeSubnetRelationshipChange } -interface NodeCapabilityTestChange { - newly_compatible: string[], - incompatible: string[] -} +// interface NodeCapabilityTestChange { +// newly_compatible: string[], +// incompatible: string[] +// } interface NodeSubnetRelationshipChange { new_gateway: Subnet[], @@ -80,12 +78,12 @@ export async function updateNode(data: Node) { (updatedNodeResponse, current) => { const updatedNode = updatedNodeResponse.node; - Object.keys(updatedNodeResponse.capability_test_changes).forEach(cap => { - let incompatible = updatedNodeResponse.capability_test_changes[cap].incompatible.map(i => testTypes.getDisplay(i)) - let newly_compatible = updatedNodeResponse.capability_test_changes[cap].newly_compatible.map(n => testTypes.getDisplay(n)) - incompatible.length > 0 ? pushWarning(`The following tests are no longer compatible with node "${updatedNode.name}" and have been removed: ${incompatible.join(", ")}`) : null - newly_compatible.length > 0 ? pushInfo(`The following tests are now compatible with node "${updatedNode.name}" and have been added: ${newly_compatible.join(", ")}`) : null - }) + // Object.keys(updatedNodeResponse.capability_test_changes).forEach(cap => { + // let incompatible = updatedNodeResponse.capability_test_changes[cap].incompatible.map(i => testTypes.getDisplay(i)) + // let newly_compatible = updatedNodeResponse.capability_test_changes[cap].newly_compatible.map(n => testTypes.getDisplay(n)) + // incompatible.length > 0 ? pushWarning(`The following tests are no longer compatible with node "${updatedNode.name}" and have been removed: ${incompatible.join(", ")}`) : null + // newly_compatible.length > 0 ? pushInfo(`The following tests are now compatible with node "${updatedNode.name}" and have been added: ${newly_compatible.join(", ")}`) : null + // }) if (updatedNodeResponse.subnet_changes.new_dns_resolver.length > 0) { pushInfo(`The following subnets now have node "${updatedNode.name}" set as a DNS resolver: ${ @@ -133,7 +131,6 @@ export function createEmptyNodeFormData(): Node { created_at: utcTimeZoneSentinel, updated_at: utcTimeZoneSentinel, name: '', - status: 'Unknown', description: '', hostname: '', target: { @@ -142,14 +139,11 @@ export function createEmptyNodeFormData(): Node { ip: '', }, }, - node_type: 'UnknownDevice', - capabilities: [], + services: [], subnets: [], - monitoring_interval: 10, last_seen: utcTimeZoneSentinel, node_groups: [], discovery_status: 'Manual', - dns_resolver_node_id: uuidv4Sentinel }; } diff --git a/src/lib/features/nodes/types/base.ts b/src/lib/features/nodes/types/base.ts index 77e5cfa4..48f109f4 100644 --- a/src/lib/features/nodes/types/base.ts +++ b/src/lib/features/nodes/types/base.ts @@ -1,12 +1,6 @@ -import type { Capability } from "$lib/features/capabilities/types/base"; +import type { Service } from "$lib/features/services/types/base"; import type { NodeTarget } from "./targets"; -export interface NodeContext { - node_id?: string; - node_type: string; - capabilities: Capability[]; - target: any; -} export interface Node { id: string; created_at: string; @@ -16,15 +10,11 @@ export interface Node { description: string; hostname: string; target: NodeTarget; - node_type: string; - capabilities: Capability[]; + services: Service[]; subnets: NodeSubnetMembership[]; - monitoring_interval: number; node_groups: string[]; discovery_status: string; - status: string; - dns_resolver_node_id: string; } export type NodeCapability = { diff --git a/src/lib/features/services/types/base.ts b/src/lib/features/services/types/base.ts new file mode 100644 index 00000000..7357d72d --- /dev/null +++ b/src/lib/features/services/types/base.ts @@ -0,0 +1,53 @@ +// Frontend Service interface that matches the backend Service enum with serde(tag = "type") + +export interface Port { + number: number; + udp: boolean; + tcp: boolean; +} + +export interface Endpoint { + url?: string; + method?: string; + protocol?: string; + ip?: string; + port?: Port; + path?: string; +} + +export interface Service { + // Service type (automatically added by serde tag) + type: string; + + // Common fields shared by all service variants + confirmed: boolean; + name: string; + ports: Port[]; + endpoints: Endpoint[]; + + // Optional daemon_id for NetvisorDaemon services + daemon_id?: string; +} + +// Helper functions for working with services and the TypeRegistry +export function createDefaultService(serviceType: string, serviceName?: string, defaultPorts?: Port[], defaultEndpoints?: Endpoint[]): Service { + return { + type: serviceType, + confirmed: false, + name: serviceName || serviceType, + ports: defaultPorts ? [...defaultPorts] : [], + endpoints: defaultEndpoints ? [...defaultEndpoints] : [] + }; +} + +export function getServiceDisplayName(service: Service): string { + return service.name || service.type; +} + +export function formatServicePorts(ports: Port[]): string { + if (!ports || ports.length === 0) return "No ports"; + + return ports.map(p => + `${p.number}${p.tcp && p.udp ? '/tcp+udp' : p.tcp ? '/tcp' : '/udp'}` + ).join(', '); +} \ No newline at end of file diff --git a/src/lib/shared/components/data/Tag.svelte b/src/lib/shared/components/data/Tag.svelte index 9676cc7f..a71fbd89 100644 --- a/src/lib/shared/components/data/Tag.svelte +++ b/src/lib/shared/components/data/Tag.svelte @@ -1,13 +1,12 @@ - -{#if formField} -
- - - {#if field.field_type.base_type === 'string'} - - - {:else if field.field_type.base_type === 'integer'} - - - {:else if field.field_type.base_type === 'boolean'} - - - {:else if field.field_type.base_type === 'select'} - - - {:else if field.field_type.base_type === 'rich_select'} - ({ - value: opt.value, - label: opt.label, - description: opt.description, - disabled: opt.disabled || false, - metadata: opt.metadata - })) || []} - placeholder={field.placeholder || 'Select an option...'} - {disabled} - onSelect={handleRichSelectChange} - getOptionIcon={(opt) => { - if (opt.metadata?.icon) { - return createStyle(null, opt.metadata.icon).IconComponent; - } - return null; - }} - getOptionIconColor={(opt) => { - if (opt.metadata?.color) { - return createStyle(opt.metadata.color, null).colors.icon; - } - return 'text-gray-400'; - }} - /> - - {:else} - - - {/if} - - - {#if field.help_text && field.field_type.base_type !== 'boolean'} -

- {field.help_text} -

- {/if} - - - {#if $formField.errors.length > 0} -
- -

{$formField.errors[0]}

-
- {/if} -
-{/if} \ No newline at end of file diff --git a/src/lib/shared/components/forms/JsonContainer.svelte b/src/lib/shared/components/forms/JsonContainer.svelte deleted file mode 100644 index 9e537d0f..00000000 --- a/src/lib/shared/components/forms/JsonContainer.svelte +++ /dev/null @@ -1,107 +0,0 @@ - - -
- -
- - - {#if showCopyButton} - - {/if} -
- - - {#if isExpanded} -
-
-
-
{prettyJson}
- - - -
-
- - -
- {jsonLines} lines • {prettyJson.length} characters - JSON -
-
- {/if} -
\ No newline at end of file diff --git a/src/lib/shared/components/forms/ListConfigEditor.svelte b/src/lib/shared/components/forms/ListConfigEditor.svelte index a85333f4..0e41f124 100644 --- a/src/lib/shared/components/forms/ListConfigEditor.svelte +++ b/src/lib/shared/components/forms/ListConfigEditor.svelte @@ -33,6 +33,7 @@ export let getOptionIconColor: (option: TOption) => string = () => ''; export let getOptionTags: (option: TOption) => TagProps[] = () => []; export let getOptionIsDisabled: (option: TOption) => boolean = () => false; + export let getOptionCategory: (item: any) => string | null = (item) => null; // Display functions for items (list) export let getItemId: (item: TItem) => string; @@ -134,6 +135,7 @@ getOptionIconColor={getOptionIconColor} getOptionTags={getOptionTags} getOptionIsDisabled={getOptionIsDisabled} + {getOptionCategory} getItemId={getItemId} getItemLabel={getItemLabel} diff --git a/src/lib/shared/components/forms/ListManager.svelte b/src/lib/shared/components/forms/ListManager.svelte index 945dac22..f84a3b62 100644 --- a/src/lib/shared/components/forms/ListManager.svelte +++ b/src/lib/shared/components/forms/ListManager.svelte @@ -29,6 +29,7 @@ export let getOptionLabel: (item: V) => string | null = (item) => null export let getOptionDescription: (item: V) => string | null = (item) => null export let getOptionIsDisabled: (item: V) => boolean = (item) => false + export let getOptionCategory: (item: any) => string | null = (item) => null; // Items export let items: T[] = []; @@ -158,6 +159,7 @@ {getOptionDescription} {getOptionTags} {getOptionIsDisabled} + {getOptionCategory} />
diff --git a/src/lib/shared/components/forms/ListSelectItem.svelte b/src/lib/shared/components/forms/ListSelectItem.svelte index 28978f4e..770e4d76 100644 --- a/src/lib/shared/components/forms/ListSelectItem.svelte +++ b/src/lib/shared/components/forms/ListSelectItem.svelte @@ -27,20 +27,22 @@
- {getLabel(item)} +
+ {getLabel(item)} + + {#if getTags} + {@const tags = getTags(item)} + {#each tags as tag} + + {/each} + {/if} +
{#if getDescription} {getDescription(item)} {/if}
- - - {#if getTags} - {@const tags = getTags(item)} - {#each tags as tag} - - {/each} - {/if}
\ No newline at end of file diff --git a/src/lib/shared/components/forms/RichSelect.svelte b/src/lib/shared/components/forms/RichSelect.svelte index 00351aa5..9f28bd03 100644 --- a/src/lib/shared/components/forms/RichSelect.svelte +++ b/src/lib/shared/components/forms/RichSelect.svelte @@ -20,6 +20,7 @@ export let getOptionLabel: (item: any) => string | null = (item) => null export let getOptionDescription: (item: any) => string | null = (item) => null export let getOptionIsDisabled: (item: any) => boolean = (item) => false + export let getOptionCategory: (item: any) => string | null = (item) => null; export let showDescriptionUnderDropdown: boolean = false; @@ -28,6 +29,32 @@ $: selectedItem = options.find(i => getOptionId(i) === selectedValue); + // Group options by category when getOptionCategory is provided + $: groupedOptions = (() => { + if (!getOptionCategory) { + return [{ category: null, options: options }]; + } + + const groups = new Map(); + + options.forEach(option => { + const category = getOptionCategory(option); + if (!groups.has(category)) { + groups.set(category, []); + } + groups.get(category)!.push(option); + }); + + // Sort categories alphabetically, with null category first + const sortedEntries = Array.from(groups.entries()).sort(([a], [b]) => { + if (a === null) return -1; + if (b === null) return 1; + return a.localeCompare(b); + }); + + return sortedEntries.map(([category, options]) => ({ category, options })); + })(); + function handleSelect(value: string) { try { const item = options.find(i => getOptionId(i) === value); @@ -115,28 +142,41 @@ {#if isOpen && !disabled}
- {#each options as option} - + {#each groupedOptions as group, groupIndex} + + {#if group.category !== null} +
+ {group.category} +
+ {/if} + + + {#each group.options as option, optionIndex} + {@const isLastInGroup = optionIndex === group.options.length - 1} + {@const isLastGroup = groupIndex === groupedOptions.length - 1} + + {/each} {/each}
{/if} diff --git a/src/lib/shared/stores/registry.ts b/src/lib/shared/stores/registry.ts index 8bd5bc12..628bea14 100644 --- a/src/lib/shared/stores/registry.ts +++ b/src/lib/shared/stores/registry.ts @@ -14,13 +14,8 @@ export interface TypeMetadata { } export interface TypeRegistry { - test_types: TypeMetadata[]; - node_types: TypeMetadata[]; - capabilities: TypeMetadata[]; - criticality_levels: TypeMetadata[]; - node_statuses: TypeMetadata[]; + services: TypeMetadata[]; node_targets: TypeMetadata[]; - diagnostic_statuses: TypeMetadata[]; } export const registry = writable(); @@ -53,6 +48,11 @@ function createRegistryHelpers(category: T) { const $registry = get(registry); return $registry?.[category]?.find(item => item.id === id)?.icon || 'help-circle'; }, + + getCategory: (id: string | null) => { + const $registry = get(registry); + return $registry?.[category]?.find(item => item.id === id)?.category || ""; + }, getColor: (id: string | null): ColorStyle => { const $registry = get(registry); @@ -86,13 +86,8 @@ function createRegistryHelpers(category: T) { } // Create all the helpers -export const testTypes = createRegistryHelpers('test_types'); -export const nodeTypes = createRegistryHelpers('node_types'); -export const capabilities = createRegistryHelpers('capabilities'); -export const criticalityLevels = createRegistryHelpers('criticality_levels'); -export const nodeStatuses = createRegistryHelpers('node_statuses'); +export const services = createRegistryHelpers('services'); export const nodeTargets = createRegistryHelpers('node_targets'); -export const diagnosticStatuses = createRegistryHelpers('diagnostic_statuses'); export async function getRegistry() { const result = await api.request(