@cwygoda/service-catalog-ui 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__mocks__/app-environment.d.ts +4 -0
- package/dist/__mocks__/app-environment.js +4 -0
- package/dist/__mocks__/app-state.d.ts +12 -0
- package/dist/__mocks__/app-state.js +10 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/adapters/static-json.adapter.d.ts +2 -0
- package/dist/adapters/static-json.adapter.js +34 -0
- package/dist/components/BpmnDiagram.svelte +496 -0
- package/dist/components/BpmnDiagram.svelte.d.ts +18 -0
- package/dist/components/Breadcrumbs.svelte +32 -0
- package/dist/components/Breadcrumbs.svelte.d.ts +11 -0
- package/dist/components/DataStoreCard.svelte +26 -0
- package/dist/components/DataStoreCard.svelte.d.ts +7 -0
- package/dist/components/DataStoreShield.svelte +67 -0
- package/dist/components/DataStoreShield.svelte.d.ts +9 -0
- package/dist/components/DomainCard.svelte +34 -0
- package/dist/components/DomainCard.svelte.d.ts +9 -0
- package/dist/components/DomainCard.test.d.ts +1 -0
- package/dist/components/DomainCard.test.js +45 -0
- package/dist/components/Header.svelte +144 -0
- package/dist/components/Header.svelte.d.ts +3 -0
- package/dist/components/NavModeToggle.svelte +43 -0
- package/dist/components/NavModeToggle.svelte.d.ts +18 -0
- package/dist/components/NavTree.svelte +245 -0
- package/dist/components/NavTree.svelte.d.ts +10 -0
- package/dist/components/SearchModal.svelte +288 -0
- package/dist/components/SearchModal.svelte.d.ts +3 -0
- package/dist/components/ServiceCard.svelte +36 -0
- package/dist/components/ServiceCard.svelte.d.ts +7 -0
- package/dist/components/ServiceCard.test.d.ts +1 -0
- package/dist/components/ServiceCard.test.js +43 -0
- package/dist/components/ServiceGraph.svelte +437 -0
- package/dist/components/ServiceGraph.svelte.d.ts +10 -0
- package/dist/components/ServiceTypeShield.svelte +32 -0
- package/dist/components/ServiceTypeShield.svelte.d.ts +8 -0
- package/dist/components/Shield.svelte +118 -0
- package/dist/components/Shield.svelte.d.ts +7 -0
- package/dist/components/ThemeToggle.svelte +44 -0
- package/dist/components/ThemeToggle.svelte.d.ts +18 -0
- package/dist/components/ThemeToggle.test.d.ts +1 -0
- package/dist/components/ThemeToggle.test.js +52 -0
- package/dist/components/UseCaseCard.svelte +61 -0
- package/dist/components/UseCaseCard.svelte.d.ts +7 -0
- package/dist/components/UseCaseCard.test.d.ts +1 -0
- package/dist/components/UseCaseCard.test.js +87 -0
- package/dist/components/index.d.ts +15 -0
- package/dist/components/index.js +15 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/ports/catalog.port.d.ts +5 -0
- package/dist/ports/catalog.port.js +1 -0
- package/dist/ports/index.d.ts +1 -0
- package/dist/ports/index.js +1 -0
- package/dist/source.css +1 -0
- package/dist/stores/index.d.ts +4 -0
- package/dist/stores/index.js +3 -0
- package/dist/stores/nav-mode.svelte.d.ts +7 -0
- package/dist/stores/nav-mode.svelte.js +32 -0
- package/dist/stores/search.svelte.d.ts +9 -0
- package/dist/stores/search.svelte.js +14 -0
- package/dist/stores/theme.svelte.d.ts +8 -0
- package/dist/stores/theme.svelte.js +61 -0
- package/dist/utils/fetch-catalog.d.ts +6 -0
- package/dist/utils/fetch-catalog.js +26 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { DataStoreType } from '@cwygoda/service-catalog-core/domain';
|
|
3
|
+
import Shield from './Shield.svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
type: DataStoreType;
|
|
7
|
+
technology?: string | undefined;
|
|
8
|
+
size?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { type, technology, size = 40 }: Props = $props();
|
|
12
|
+
|
|
13
|
+
const typeLabels: Record<DataStoreType, { short: string; full: string }> = {
|
|
14
|
+
database: { short: 'Db', full: 'Database' },
|
|
15
|
+
cache: { short: 'Ca', full: 'Cache' },
|
|
16
|
+
queue: { short: 'Qu', full: 'Queue' },
|
|
17
|
+
'search-index': { short: 'Si', full: 'Search Index' },
|
|
18
|
+
'object-store': { short: 'Os', full: 'Object Store' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const techLabels: Record<string, { short: string; full: string } | undefined> = {
|
|
22
|
+
postgresql: { short: 'Pg', full: 'PostgreSQL' },
|
|
23
|
+
postgres: { short: 'Pg', full: 'PostgreSQL' },
|
|
24
|
+
'rds postgresql': { short: 'Pg', full: 'PostgreSQL' },
|
|
25
|
+
mysql: { short: 'My', full: 'MySQL' },
|
|
26
|
+
'rds mysql': { short: 'My', full: 'MySQL' },
|
|
27
|
+
mariadb: { short: 'Ma', full: 'MariaDB' },
|
|
28
|
+
mongodb: { short: 'Mg', full: 'MongoDB' },
|
|
29
|
+
redis: { short: 'Re', full: 'Redis' },
|
|
30
|
+
elasticache: { short: 'Re', full: 'ElastiCache' },
|
|
31
|
+
memcached: { short: 'Mc', full: 'Memcached' },
|
|
32
|
+
elasticsearch: { short: 'Es', full: 'Elasticsearch' },
|
|
33
|
+
opensearch: { short: 'Os', full: 'OpenSearch' },
|
|
34
|
+
s3: { short: 'S3', full: 'Amazon S3' },
|
|
35
|
+
'amazon s3': { short: 'S3', full: 'Amazon S3' },
|
|
36
|
+
dynamodb: { short: 'Dy', full: 'DynamoDB' },
|
|
37
|
+
kafka: { short: 'Kf', full: 'Kafka' },
|
|
38
|
+
rabbitmq: { short: 'Rq', full: 'RabbitMQ' },
|
|
39
|
+
sqs: { short: 'Sq', full: 'Amazon SQS' },
|
|
40
|
+
'amazon sqs': { short: 'Sq', full: 'Amazon SQS' },
|
|
41
|
+
sqlite: { short: 'Sl', full: 'SQLite' },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let entry = $derived.by(() => {
|
|
45
|
+
if (technology) {
|
|
46
|
+
const match = techLabels[technology.toLowerCase()];
|
|
47
|
+
if (match) return match;
|
|
48
|
+
}
|
|
49
|
+
return typeLabels[type];
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
let tooltip = $derived(
|
|
53
|
+
technology && entry.full !== technology
|
|
54
|
+
? `${entry.full} (${typeLabels[type].full})`
|
|
55
|
+
: entry.full
|
|
56
|
+
);
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<span class="group relative inline-flex" title={tooltip}>
|
|
60
|
+
<Shield label={entry.short} {size} />
|
|
61
|
+
<span
|
|
62
|
+
role="tooltip"
|
|
63
|
+
class="pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2 whitespace-nowrap rounded bg-gray-900 px-2 py-1 text-xs font-medium text-white opacity-0 shadow-lg transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 dark:bg-gray-100 dark:text-gray-900"
|
|
64
|
+
>
|
|
65
|
+
{tooltip}
|
|
66
|
+
</span>
|
|
67
|
+
</span>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { DataStoreType } from '@cwygoda/service-catalog-core/domain';
|
|
2
|
+
interface Props {
|
|
3
|
+
type: DataStoreType;
|
|
4
|
+
technology?: string | undefined;
|
|
5
|
+
size?: number;
|
|
6
|
+
}
|
|
7
|
+
declare const DataStoreShield: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type DataStoreShield = ReturnType<typeof DataStoreShield>;
|
|
9
|
+
export default DataStoreShield;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Domain } from '@cwygoda/service-catalog-core/domain';
|
|
3
|
+
import Shield from './Shield.svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
domain: Domain;
|
|
7
|
+
useCaseCount: number;
|
|
8
|
+
serviceCount: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { domain, useCaseCount, serviceCount }: Props = $props();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<a
|
|
15
|
+
href="/domains/{domain.id}"
|
|
16
|
+
aria-label="View {domain.name} domain"
|
|
17
|
+
class="block rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md active:shadow-sm dark:border-gray-700 dark:bg-gray-800"
|
|
18
|
+
>
|
|
19
|
+
<div class="mb-3 flex items-start gap-4">
|
|
20
|
+
<Shield label={domain.name} size={44} />
|
|
21
|
+
<div class="min-w-0 flex-1">
|
|
22
|
+
<h3 class="truncate text-lg font-semibold text-gray-900 dark:text-white">
|
|
23
|
+
{domain.name}
|
|
24
|
+
</h3>
|
|
25
|
+
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
26
|
+
{domain.description}
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="flex gap-4 text-xs text-gray-500 dark:text-gray-400">
|
|
31
|
+
<span>{useCaseCount} use case{useCaseCount !== 1 ? 's' : ''}</span>
|
|
32
|
+
<span>{serviceCount} service{serviceCount !== 1 ? 's' : ''}</span>
|
|
33
|
+
</div>
|
|
34
|
+
</a>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Domain } from '@cwygoda/service-catalog-core/domain';
|
|
2
|
+
interface Props {
|
|
3
|
+
domain: Domain;
|
|
4
|
+
useCaseCount: number;
|
|
5
|
+
serviceCount: number;
|
|
6
|
+
}
|
|
7
|
+
declare const DomainCard: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type DomainCard = ReturnType<typeof DomainCard>;
|
|
9
|
+
export default DomainCard;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/// <reference types="@testing-library/jest-dom" />
|
|
2
|
+
import { render, screen } from '@testing-library/svelte';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import DomainCard from './DomainCard.svelte';
|
|
5
|
+
describe('DomainCard', () => {
|
|
6
|
+
const baseDomain = {
|
|
7
|
+
id: 'commerce',
|
|
8
|
+
name: 'Commerce',
|
|
9
|
+
description: 'E-commerce domain',
|
|
10
|
+
};
|
|
11
|
+
it('renders domain name', () => {
|
|
12
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 2, serviceCount: 3 } });
|
|
13
|
+
expect(screen.getByText('Commerce')).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
it('renders domain description', () => {
|
|
16
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 2, serviceCount: 3 } });
|
|
17
|
+
expect(screen.getByText('E-commerce domain')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
it('links to domain detail page', () => {
|
|
20
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 2, serviceCount: 3 } });
|
|
21
|
+
const link = screen.getByRole('link');
|
|
22
|
+
expect(link).toHaveAttribute('href', '/domains/commerce');
|
|
23
|
+
});
|
|
24
|
+
it('shows use case count', () => {
|
|
25
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 2, serviceCount: 3 } });
|
|
26
|
+
expect(screen.getByText('2 use cases')).toBeInTheDocument();
|
|
27
|
+
});
|
|
28
|
+
it('shows singular use case for count of 1', () => {
|
|
29
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 1, serviceCount: 3 } });
|
|
30
|
+
expect(screen.getByText('1 use case')).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
it('shows service count', () => {
|
|
33
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 2, serviceCount: 3 } });
|
|
34
|
+
expect(screen.getByText('3 services')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
it('shows singular service for count of 1', () => {
|
|
37
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 2, serviceCount: 1 } });
|
|
38
|
+
expect(screen.getByText('1 service')).toBeInTheDocument();
|
|
39
|
+
});
|
|
40
|
+
it('shows zero counts correctly', () => {
|
|
41
|
+
render(DomainCard, { props: { domain: baseDomain, useCaseCount: 0, serviceCount: 0 } });
|
|
42
|
+
expect(screen.getByText('0 use cases')).toBeInTheDocument();
|
|
43
|
+
expect(screen.getByText('0 services')).toBeInTheDocument();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
import ThemeToggle from './ThemeToggle.svelte';
|
|
4
|
+
import NavModeToggle from './NavModeToggle.svelte';
|
|
5
|
+
import { searchStore } from '../stores/search.svelte.js';
|
|
6
|
+
|
|
7
|
+
const navLinks = [
|
|
8
|
+
{ href: '/', label: 'Home' },
|
|
9
|
+
{ href: '/domains', label: 'Domains' },
|
|
10
|
+
{ href: '/use-cases', label: 'Use Cases' },
|
|
11
|
+
{ href: '/services', label: 'Services' },
|
|
12
|
+
{ href: '/data-stores', label: 'Data Stores' },
|
|
13
|
+
{ href: '/graph', label: 'Graph' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
let mobileMenuOpen = $state(false);
|
|
17
|
+
|
|
18
|
+
function isActive(href: string): boolean {
|
|
19
|
+
if (href === '/') {
|
|
20
|
+
return page.url.pathname === '/';
|
|
21
|
+
}
|
|
22
|
+
return page.url.pathname.startsWith(href);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toggleMobileMenu() {
|
|
26
|
+
mobileMenuOpen = !mobileMenuOpen;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function closeMobileMenu() {
|
|
30
|
+
mobileMenuOpen = false;
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<header class="border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900">
|
|
35
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
36
|
+
<div class="flex h-16 items-center justify-between">
|
|
37
|
+
<div class="flex items-center">
|
|
38
|
+
<a href="/" class="text-xl font-bold text-gray-900 dark:text-white"> Service Catalog </a>
|
|
39
|
+
<!-- Desktop nav -->
|
|
40
|
+
<nav class="ml-10 hidden space-x-4 sm:flex">
|
|
41
|
+
{#each navLinks as link (link.href)}
|
|
42
|
+
<a
|
|
43
|
+
href={link.href}
|
|
44
|
+
class="rounded-md px-3 py-2 text-sm font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 {isActive(
|
|
45
|
+
link.href
|
|
46
|
+
)
|
|
47
|
+
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-200'
|
|
48
|
+
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white'}"
|
|
49
|
+
>
|
|
50
|
+
{link.label}
|
|
51
|
+
</a>
|
|
52
|
+
{/each}
|
|
53
|
+
</nav>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="flex items-center gap-2">
|
|
57
|
+
<!-- Search trigger -->
|
|
58
|
+
<button
|
|
59
|
+
onclick={() => {
|
|
60
|
+
searchStore.show();
|
|
61
|
+
}}
|
|
62
|
+
class="flex items-center gap-1.5 rounded-md border border-gray-300 px-2.5 py-1.5 text-sm text-gray-500 transition-colors hover:border-gray-400 hover:text-gray-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 dark:border-gray-600 dark:text-gray-400 dark:hover:border-gray-500 dark:hover:text-gray-300"
|
|
63
|
+
aria-label="Search catalog"
|
|
64
|
+
>
|
|
65
|
+
<svg
|
|
66
|
+
class="h-4 w-4"
|
|
67
|
+
fill="none"
|
|
68
|
+
viewBox="0 0 24 24"
|
|
69
|
+
stroke="currentColor"
|
|
70
|
+
stroke-width="2"
|
|
71
|
+
aria-hidden="true"
|
|
72
|
+
>
|
|
73
|
+
<path
|
|
74
|
+
stroke-linecap="round"
|
|
75
|
+
stroke-linejoin="round"
|
|
76
|
+
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
|
|
77
|
+
/>
|
|
78
|
+
</svg>
|
|
79
|
+
<span class="hidden sm:inline">Search</span>
|
|
80
|
+
<kbd
|
|
81
|
+
class="hidden rounded border border-gray-300 px-1 py-0.5 text-xs sm:inline dark:border-gray-600"
|
|
82
|
+
>⌘K</kbd
|
|
83
|
+
>
|
|
84
|
+
</button>
|
|
85
|
+
<NavModeToggle />
|
|
86
|
+
<ThemeToggle />
|
|
87
|
+
<!-- Mobile menu button -->
|
|
88
|
+
<button
|
|
89
|
+
onclick={toggleMobileMenu}
|
|
90
|
+
class="rounded-md p-2 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 sm:hidden dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
|
91
|
+
aria-label="Toggle menu"
|
|
92
|
+
aria-expanded={mobileMenuOpen}
|
|
93
|
+
>
|
|
94
|
+
{#if mobileMenuOpen}
|
|
95
|
+
<!-- Close icon -->
|
|
96
|
+
<svg
|
|
97
|
+
class="h-6 w-6"
|
|
98
|
+
fill="none"
|
|
99
|
+
viewBox="0 0 24 24"
|
|
100
|
+
stroke="currentColor"
|
|
101
|
+
stroke-width="2"
|
|
102
|
+
aria-hidden="true"
|
|
103
|
+
>
|
|
104
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
105
|
+
</svg>
|
|
106
|
+
{:else}
|
|
107
|
+
<!-- Hamburger icon -->
|
|
108
|
+
<svg
|
|
109
|
+
class="h-6 w-6"
|
|
110
|
+
fill="none"
|
|
111
|
+
viewBox="0 0 24 24"
|
|
112
|
+
stroke="currentColor"
|
|
113
|
+
stroke-width="2"
|
|
114
|
+
aria-hidden="true"
|
|
115
|
+
>
|
|
116
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
|
117
|
+
</svg>
|
|
118
|
+
{/if}
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- Mobile menu -->
|
|
125
|
+
{#if mobileMenuOpen}
|
|
126
|
+
<nav
|
|
127
|
+
class="border-t border-gray-200 bg-white px-4 py-3 sm:hidden dark:border-gray-700 dark:bg-gray-900"
|
|
128
|
+
>
|
|
129
|
+
<div class="flex flex-col space-y-1">
|
|
130
|
+
{#each navLinks as link (link.href)}
|
|
131
|
+
<a
|
|
132
|
+
href={link.href}
|
|
133
|
+
onclick={closeMobileMenu}
|
|
134
|
+
class="rounded-md px-3 py-2 text-base font-medium transition-colors {isActive(link.href)
|
|
135
|
+
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-200'
|
|
136
|
+
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white'}"
|
|
137
|
+
>
|
|
138
|
+
{link.label}
|
|
139
|
+
</a>
|
|
140
|
+
{/each}
|
|
141
|
+
</div>
|
|
142
|
+
</nav>
|
|
143
|
+
{/if}
|
|
144
|
+
</header>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { navModeStore } from '../stores/nav-mode.svelte.js';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<button
|
|
6
|
+
onclick={() => {
|
|
7
|
+
navModeStore.toggle();
|
|
8
|
+
}}
|
|
9
|
+
class="hidden rounded-md p-2 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 lg:block dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
|
10
|
+
aria-label={navModeStore.mode === 'flat' ? 'Switch to tree view' : 'Switch to flat view'}
|
|
11
|
+
title={navModeStore.mode === 'flat' ? 'Switch to tree view' : 'Switch to flat view'}
|
|
12
|
+
>
|
|
13
|
+
<span class="sr-only" aria-live="polite">Navigation: {navModeStore.mode} view</span>
|
|
14
|
+
{#if navModeStore.mode === 'flat'}
|
|
15
|
+
<!-- List icon -->
|
|
16
|
+
<svg
|
|
17
|
+
class="h-5 w-5"
|
|
18
|
+
fill="none"
|
|
19
|
+
viewBox="0 0 24 24"
|
|
20
|
+
stroke="currentColor"
|
|
21
|
+
stroke-width="2"
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
>
|
|
24
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
|
25
|
+
</svg>
|
|
26
|
+
{:else}
|
|
27
|
+
<!-- Tree icon -->
|
|
28
|
+
<svg
|
|
29
|
+
class="h-5 w-5"
|
|
30
|
+
fill="none"
|
|
31
|
+
viewBox="0 0 24 24"
|
|
32
|
+
stroke="currentColor"
|
|
33
|
+
stroke-width="2"
|
|
34
|
+
aria-hidden="true"
|
|
35
|
+
>
|
|
36
|
+
<path
|
|
37
|
+
stroke-linecap="round"
|
|
38
|
+
stroke-linejoin="round"
|
|
39
|
+
d="M3 7h2m4 0h12M3 12h2m4 0h12M3 17h2m4 0h12"
|
|
40
|
+
/>
|
|
41
|
+
</svg>
|
|
42
|
+
{/if}
|
|
43
|
+
</button>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const NavModeToggle: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type NavModeToggle = InstanceType<typeof NavModeToggle>;
|
|
18
|
+
export default NavModeToggle;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
3
|
+
import type { Domain, UseCase, Service, DataStore } from '@cwygoda/service-catalog-core/domain';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
domains: Domain[];
|
|
7
|
+
useCases: UseCase[];
|
|
8
|
+
services: Service[];
|
|
9
|
+
dataStores?: DataStore[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { domains, useCases, services, dataStores = [] }: Props = $props();
|
|
13
|
+
|
|
14
|
+
// Track expanded state for domains and use cases
|
|
15
|
+
const expandedDomains = new SvelteSet<string>();
|
|
16
|
+
const expandedUseCases = new SvelteSet<string>();
|
|
17
|
+
|
|
18
|
+
function toggleDomain(id: string) {
|
|
19
|
+
if (expandedDomains.has(id)) {
|
|
20
|
+
expandedDomains.delete(id);
|
|
21
|
+
} else {
|
|
22
|
+
expandedDomains.add(id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function toggleUseCase(id: string) {
|
|
27
|
+
if (expandedUseCases.has(id)) {
|
|
28
|
+
expandedUseCases.delete(id);
|
|
29
|
+
} else {
|
|
30
|
+
expandedUseCases.add(id);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Pre-computed lookup maps — O(1) per query instead of O(n) filter scans
|
|
35
|
+
const rootDomains = $derived(domains.filter((d) => !d.parent));
|
|
36
|
+
|
|
37
|
+
const childDomainMap = $derived.by(() => {
|
|
38
|
+
const map = new SvelteMap<string, Domain[]>();
|
|
39
|
+
for (const d of domains) {
|
|
40
|
+
if (d.parent) {
|
|
41
|
+
const arr = map.get(d.parent);
|
|
42
|
+
if (arr) arr.push(d);
|
|
43
|
+
else map.set(d.parent, [d]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return map;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const domainUseCaseMap = $derived.by(() => {
|
|
50
|
+
const map = new SvelteMap<string, UseCase[]>();
|
|
51
|
+
for (const uc of useCases) {
|
|
52
|
+
if (!uc.domain) continue;
|
|
53
|
+
const arr = map.get(uc.domain);
|
|
54
|
+
if (arr) arr.push(uc);
|
|
55
|
+
else map.set(uc.domain, [uc]);
|
|
56
|
+
}
|
|
57
|
+
return map;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const domainServiceMap = $derived.by(() => {
|
|
61
|
+
const map = new SvelteMap<string, Service[]>();
|
|
62
|
+
for (const s of services) {
|
|
63
|
+
if (!s.domain) continue;
|
|
64
|
+
const arr = map.get(s.domain);
|
|
65
|
+
if (arr) arr.push(s);
|
|
66
|
+
else map.set(s.domain, [s]);
|
|
67
|
+
}
|
|
68
|
+
return map;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const domainDataStoreMap = $derived.by(() => {
|
|
72
|
+
const map = new SvelteMap<string, DataStore[]>();
|
|
73
|
+
for (const ds of dataStores) {
|
|
74
|
+
if (!ds.domain) continue;
|
|
75
|
+
const arr = map.get(ds.domain);
|
|
76
|
+
if (arr) arr.push(ds);
|
|
77
|
+
else map.set(ds.domain, [ds]);
|
|
78
|
+
}
|
|
79
|
+
return map;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const serviceById = $derived.by(() => new SvelteMap(services.map((s) => [s.id, s] as const)));
|
|
83
|
+
|
|
84
|
+
// Thin wrappers — keep template call sites unchanged
|
|
85
|
+
function getChildDomains(parentId: string): Domain[] {
|
|
86
|
+
return childDomainMap.get(parentId) ?? [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getDomainUseCases(domainId: string): UseCase[] {
|
|
90
|
+
return domainUseCaseMap.get(domainId) ?? [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getDomainServices(domainId: string): Service[] {
|
|
94
|
+
return domainServiceMap.get(domainId) ?? [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getDomainDataStores(domainId: string): DataStore[] {
|
|
98
|
+
return domainDataStoreMap.get(domainId) ?? [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getUseCaseServices(useCase: UseCase): Service[] {
|
|
102
|
+
return useCase.participants
|
|
103
|
+
.map((p) => serviceById.get(p.service))
|
|
104
|
+
.filter((s): s is Service => s !== undefined);
|
|
105
|
+
}
|
|
106
|
+
</script>
|
|
107
|
+
|
|
108
|
+
<nav class="text-sm" aria-label="Catalog tree">
|
|
109
|
+
<ul class="space-y-1">
|
|
110
|
+
{#each rootDomains as domain (domain.id)}
|
|
111
|
+
<li>
|
|
112
|
+
<div class="flex items-center gap-1">
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onclick={() => {
|
|
116
|
+
toggleDomain(domain.id);
|
|
117
|
+
}}
|
|
118
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded text-gray-500 hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary-600 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
119
|
+
aria-expanded={expandedDomains.has(domain.id)}
|
|
120
|
+
aria-label={expandedDomains.has(domain.id) ? 'Collapse' : 'Expand'}
|
|
121
|
+
>
|
|
122
|
+
<svg
|
|
123
|
+
class="h-4 w-4 transition-transform {expandedDomains.has(domain.id)
|
|
124
|
+
? 'rotate-90'
|
|
125
|
+
: ''}"
|
|
126
|
+
fill="currentColor"
|
|
127
|
+
viewBox="0 0 20 20"
|
|
128
|
+
aria-hidden="true"
|
|
129
|
+
>
|
|
130
|
+
<path
|
|
131
|
+
fill-rule="evenodd"
|
|
132
|
+
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
133
|
+
clip-rule="evenodd"
|
|
134
|
+
/>
|
|
135
|
+
</svg>
|
|
136
|
+
</button>
|
|
137
|
+
<a
|
|
138
|
+
href="/domains/{domain.id}"
|
|
139
|
+
class="flex-1 rounded px-2 py-1 font-medium text-gray-900 hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700"
|
|
140
|
+
>
|
|
141
|
+
{domain.name}
|
|
142
|
+
</a>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{#if expandedDomains.has(domain.id)}
|
|
146
|
+
<ul class="ml-6 mt-1 space-y-1 border-l border-gray-200 pl-2 dark:border-gray-700">
|
|
147
|
+
<!-- Child Domains -->
|
|
148
|
+
{#each getChildDomains(domain.id) as child (child.id)}
|
|
149
|
+
<li>
|
|
150
|
+
<a
|
|
151
|
+
href="/domains/{child.id}"
|
|
152
|
+
class="block rounded px-2 py-1 text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
153
|
+
>
|
|
154
|
+
{child.name}
|
|
155
|
+
</a>
|
|
156
|
+
</li>
|
|
157
|
+
{/each}
|
|
158
|
+
|
|
159
|
+
<!-- Use Cases -->
|
|
160
|
+
{#each getDomainUseCases(domain.id) as useCase (useCase.id)}
|
|
161
|
+
<li>
|
|
162
|
+
<div class="flex items-center gap-1">
|
|
163
|
+
{#if getUseCaseServices(useCase).length > 0}
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
onclick={() => {
|
|
167
|
+
toggleUseCase(useCase.id);
|
|
168
|
+
}}
|
|
169
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded text-gray-400 hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary-600 dark:hover:bg-gray-700"
|
|
170
|
+
aria-expanded={expandedUseCases.has(useCase.id)}
|
|
171
|
+
aria-label={expandedUseCases.has(useCase.id) ? 'Collapse' : 'Expand'}
|
|
172
|
+
>
|
|
173
|
+
<svg
|
|
174
|
+
class="h-4 w-4 transition-transform {expandedUseCases.has(useCase.id)
|
|
175
|
+
? 'rotate-90'
|
|
176
|
+
: ''}"
|
|
177
|
+
fill="currentColor"
|
|
178
|
+
viewBox="0 0 20 20"
|
|
179
|
+
>
|
|
180
|
+
<path
|
|
181
|
+
fill-rule="evenodd"
|
|
182
|
+
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
183
|
+
clip-rule="evenodd"
|
|
184
|
+
/>
|
|
185
|
+
</svg>
|
|
186
|
+
</button>
|
|
187
|
+
{:else}
|
|
188
|
+
<span class="min-w-11"></span>
|
|
189
|
+
{/if}
|
|
190
|
+
<a
|
|
191
|
+
href="/use-cases/{useCase.id}"
|
|
192
|
+
class="flex-1 rounded px-2 py-1 text-primary-600 hover:bg-gray-100 dark:text-primary-400 dark:hover:bg-gray-700"
|
|
193
|
+
>
|
|
194
|
+
{useCase.name}
|
|
195
|
+
</a>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{#if expandedUseCases.has(useCase.id)}
|
|
199
|
+
<ul
|
|
200
|
+
class="ml-6 mt-1 space-y-1 border-l border-gray-200 pl-2 dark:border-gray-700"
|
|
201
|
+
>
|
|
202
|
+
{#each getUseCaseServices(useCase) as service (service.id)}
|
|
203
|
+
<li>
|
|
204
|
+
<a
|
|
205
|
+
href="/services/{service.id}"
|
|
206
|
+
class="block rounded px-2 py-1 text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
207
|
+
>
|
|
208
|
+
{service.name}
|
|
209
|
+
</a>
|
|
210
|
+
</li>
|
|
211
|
+
{/each}
|
|
212
|
+
</ul>
|
|
213
|
+
{/if}
|
|
214
|
+
</li>
|
|
215
|
+
{/each}
|
|
216
|
+
|
|
217
|
+
<!-- Services directly in domain (not via use case) -->
|
|
218
|
+
{#each getDomainServices(domain.id) as service (service.id)}
|
|
219
|
+
<li>
|
|
220
|
+
<a
|
|
221
|
+
href="/services/{service.id}"
|
|
222
|
+
class="ml-5 block rounded px-2 py-1 text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
223
|
+
>
|
|
224
|
+
{service.name}
|
|
225
|
+
</a>
|
|
226
|
+
</li>
|
|
227
|
+
{/each}
|
|
228
|
+
|
|
229
|
+
<!-- Data Stores -->
|
|
230
|
+
{#each getDomainDataStores(domain.id) as ds (ds.id)}
|
|
231
|
+
<li>
|
|
232
|
+
<a
|
|
233
|
+
href="/data-stores/{ds.id}"
|
|
234
|
+
class="ml-5 block rounded px-2 py-1 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
235
|
+
>
|
|
236
|
+
{ds.name}
|
|
237
|
+
</a>
|
|
238
|
+
</li>
|
|
239
|
+
{/each}
|
|
240
|
+
</ul>
|
|
241
|
+
{/if}
|
|
242
|
+
</li>
|
|
243
|
+
{/each}
|
|
244
|
+
</ul>
|
|
245
|
+
</nav>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Domain, UseCase, Service, DataStore } from '@cwygoda/service-catalog-core/domain';
|
|
2
|
+
interface Props {
|
|
3
|
+
domains: Domain[];
|
|
4
|
+
useCases: UseCase[];
|
|
5
|
+
services: Service[];
|
|
6
|
+
dataStores?: DataStore[];
|
|
7
|
+
}
|
|
8
|
+
declare const NavTree: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type NavTree = ReturnType<typeof NavTree>;
|
|
10
|
+
export default NavTree;
|