@cwygoda/service-catalog-ui 1.0.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.
Potentially problematic release.
This version of @cwygoda/service-catalog-ui might be problematic. Click here for more details.
- 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 +360 -0
- package/dist/components/BpmnDiagram.svelte.d.ts +7 -0
- package/dist/components/Breadcrumbs.svelte +32 -0
- package/dist/components/Breadcrumbs.svelte.d.ts +11 -0
- package/dist/components/DomainCard.svelte +29 -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 +110 -0
- package/dist/components/Header.svelte.d.ts +3 -0
- package/dist/components/NavModeToggle.svelte +28 -0
- package/dist/components/NavModeToggle.svelte.d.ts +18 -0
- package/dist/components/NavTree.svelte +182 -0
- package/dist/components/NavTree.svelte.d.ts +9 -0
- package/dist/components/ServiceCard.svelte +26 -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 +36 -0
- package/dist/components/ServiceGraph.svelte +348 -0
- package/dist/components/ServiceGraph.svelte.d.ts +10 -0
- package/dist/components/ThemeToggle.svelte +29 -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 +57 -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 +10 -0
- package/dist/components/index.js +10 -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/stores/index.d.ts +3 -0
- package/dist/stores/index.js +2 -0
- package/dist/stores/nav-mode.svelte.d.ts +7 -0
- package/dist/stores/nav-mode.svelte.js +32 -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 +54 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from '$app/state';
|
|
3
|
+
import ThemeToggle from './ThemeToggle.svelte';
|
|
4
|
+
import NavModeToggle from './NavModeToggle.svelte';
|
|
5
|
+
|
|
6
|
+
const navLinks = [
|
|
7
|
+
{ href: '/', label: 'Home' },
|
|
8
|
+
{ href: '/domains', label: 'Domains' },
|
|
9
|
+
{ href: '/use-cases', label: 'Use Cases' },
|
|
10
|
+
{ href: '/services', label: 'Services' },
|
|
11
|
+
{ href: '/graph', label: 'Graph' },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
let mobileMenuOpen = $state(false);
|
|
15
|
+
|
|
16
|
+
function isActive(href: string): boolean {
|
|
17
|
+
if (href === '/') {
|
|
18
|
+
return page.url.pathname === '/';
|
|
19
|
+
}
|
|
20
|
+
return page.url.pathname.startsWith(href);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toggleMobileMenu() {
|
|
24
|
+
mobileMenuOpen = !mobileMenuOpen;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function closeMobileMenu() {
|
|
28
|
+
mobileMenuOpen = false;
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<header class="border-b border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900">
|
|
33
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
34
|
+
<div class="flex h-16 items-center justify-between">
|
|
35
|
+
<div class="flex items-center">
|
|
36
|
+
<a href="/" class="text-xl font-bold text-gray-900 dark:text-white"> Service Catalog </a>
|
|
37
|
+
<!-- Desktop nav -->
|
|
38
|
+
<nav class="ml-10 hidden space-x-4 sm:flex">
|
|
39
|
+
{#each navLinks as link (link.href)}
|
|
40
|
+
<a
|
|
41
|
+
href={link.href}
|
|
42
|
+
class="rounded-md px-3 py-2 text-sm font-medium transition-colors {isActive(link.href)
|
|
43
|
+
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-200'
|
|
44
|
+
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white'}"
|
|
45
|
+
>
|
|
46
|
+
{link.label}
|
|
47
|
+
</a>
|
|
48
|
+
{/each}
|
|
49
|
+
</nav>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="flex items-center gap-2">
|
|
53
|
+
<NavModeToggle />
|
|
54
|
+
<ThemeToggle />
|
|
55
|
+
<!-- Mobile menu button -->
|
|
56
|
+
<button
|
|
57
|
+
onclick={toggleMobileMenu}
|
|
58
|
+
class="rounded-md p-2 text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900 sm:hidden dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
|
59
|
+
aria-label="Toggle menu"
|
|
60
|
+
aria-expanded={mobileMenuOpen}
|
|
61
|
+
>
|
|
62
|
+
{#if mobileMenuOpen}
|
|
63
|
+
<!-- Close icon -->
|
|
64
|
+
<svg
|
|
65
|
+
class="h-6 w-6"
|
|
66
|
+
fill="none"
|
|
67
|
+
viewBox="0 0 24 24"
|
|
68
|
+
stroke="currentColor"
|
|
69
|
+
stroke-width="2"
|
|
70
|
+
>
|
|
71
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
72
|
+
</svg>
|
|
73
|
+
{:else}
|
|
74
|
+
<!-- Hamburger icon -->
|
|
75
|
+
<svg
|
|
76
|
+
class="h-6 w-6"
|
|
77
|
+
fill="none"
|
|
78
|
+
viewBox="0 0 24 24"
|
|
79
|
+
stroke="currentColor"
|
|
80
|
+
stroke-width="2"
|
|
81
|
+
>
|
|
82
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
|
83
|
+
</svg>
|
|
84
|
+
{/if}
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<!-- Mobile menu -->
|
|
91
|
+
{#if mobileMenuOpen}
|
|
92
|
+
<nav
|
|
93
|
+
class="border-t border-gray-200 bg-white px-4 py-3 sm:hidden dark:border-gray-700 dark:bg-gray-900"
|
|
94
|
+
>
|
|
95
|
+
<div class="flex flex-col space-y-1">
|
|
96
|
+
{#each navLinks as link (link.href)}
|
|
97
|
+
<a
|
|
98
|
+
href={link.href}
|
|
99
|
+
onclick={closeMobileMenu}
|
|
100
|
+
class="rounded-md px-3 py-2 text-base font-medium transition-colors {isActive(link.href)
|
|
101
|
+
? 'bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-200'
|
|
102
|
+
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white'}"
|
|
103
|
+
>
|
|
104
|
+
{link.label}
|
|
105
|
+
</a>
|
|
106
|
+
{/each}
|
|
107
|
+
</div>
|
|
108
|
+
</nav>
|
|
109
|
+
{/if}
|
|
110
|
+
</header>
|
|
@@ -0,0 +1,28 @@
|
|
|
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 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
|
+
{#if navModeStore.mode === 'flat'}
|
|
14
|
+
<!-- List icon -->
|
|
15
|
+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
16
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
|
17
|
+
</svg>
|
|
18
|
+
{:else}
|
|
19
|
+
<!-- Tree icon -->
|
|
20
|
+
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
21
|
+
<path
|
|
22
|
+
stroke-linecap="round"
|
|
23
|
+
stroke-linejoin="round"
|
|
24
|
+
d="M3 7h2m4 0h12M3 12h2m4 0h12M3 17h2m4 0h12"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
{/if}
|
|
28
|
+
</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,182 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { SvelteSet } from 'svelte/reactivity';
|
|
3
|
+
import type { Domain, UseCase, Service } from '@cwygoda/service-catalog-core/domain';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
domains: Domain[];
|
|
7
|
+
useCases: UseCase[];
|
|
8
|
+
services: Service[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { domains, useCases, services }: Props = $props();
|
|
12
|
+
|
|
13
|
+
// Track expanded state for domains and use cases
|
|
14
|
+
const expandedDomains = new SvelteSet<string>();
|
|
15
|
+
const expandedUseCases = new SvelteSet<string>();
|
|
16
|
+
|
|
17
|
+
function toggleDomain(id: string) {
|
|
18
|
+
if (expandedDomains.has(id)) {
|
|
19
|
+
expandedDomains.delete(id);
|
|
20
|
+
} else {
|
|
21
|
+
expandedDomains.add(id);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function toggleUseCase(id: string) {
|
|
26
|
+
if (expandedUseCases.has(id)) {
|
|
27
|
+
expandedUseCases.delete(id);
|
|
28
|
+
} else {
|
|
29
|
+
expandedUseCases.add(id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Get root domains (no parent)
|
|
34
|
+
const rootDomains = $derived(domains.filter((d) => !d.parent));
|
|
35
|
+
|
|
36
|
+
// Helper to get child domains
|
|
37
|
+
function getChildDomains(parentId: string): Domain[] {
|
|
38
|
+
return domains.filter((d) => d.parent === parentId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Helper to get use cases for a domain
|
|
42
|
+
function getDomainUseCases(domainId: string): UseCase[] {
|
|
43
|
+
return useCases.filter((uc) => uc.domain === domainId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Helper to get services for a domain
|
|
47
|
+
function getDomainServices(domainId: string): Service[] {
|
|
48
|
+
return services.filter((s) => s.domain === domainId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Helper to get services for a use case
|
|
52
|
+
function getUseCaseServices(useCase: UseCase): Service[] {
|
|
53
|
+
const serviceIds = useCase.participants.map((p) => p.service);
|
|
54
|
+
return services.filter((s) => serviceIds.includes(s.id));
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<nav class="text-sm" aria-label="Catalog tree">
|
|
59
|
+
<ul class="space-y-1">
|
|
60
|
+
{#each rootDomains as domain (domain.id)}
|
|
61
|
+
<li>
|
|
62
|
+
<div class="flex items-center gap-1">
|
|
63
|
+
<button
|
|
64
|
+
type="button"
|
|
65
|
+
onclick={() => {
|
|
66
|
+
toggleDomain(domain.id);
|
|
67
|
+
}}
|
|
68
|
+
class="flex h-5 w-5 items-center justify-center rounded text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
69
|
+
aria-expanded={expandedDomains.has(domain.id)}
|
|
70
|
+
aria-label={expandedDomains.has(domain.id) ? 'Collapse' : 'Expand'}
|
|
71
|
+
>
|
|
72
|
+
<svg
|
|
73
|
+
class="h-3 w-3 transition-transform {expandedDomains.has(domain.id)
|
|
74
|
+
? 'rotate-90'
|
|
75
|
+
: ''}"
|
|
76
|
+
fill="currentColor"
|
|
77
|
+
viewBox="0 0 20 20"
|
|
78
|
+
>
|
|
79
|
+
<path
|
|
80
|
+
fill-rule="evenodd"
|
|
81
|
+
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"
|
|
82
|
+
clip-rule="evenodd"
|
|
83
|
+
/>
|
|
84
|
+
</svg>
|
|
85
|
+
</button>
|
|
86
|
+
<a
|
|
87
|
+
href="/domains/{domain.id}"
|
|
88
|
+
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"
|
|
89
|
+
>
|
|
90
|
+
{domain.name}
|
|
91
|
+
</a>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{#if expandedDomains.has(domain.id)}
|
|
95
|
+
<ul class="ml-6 mt-1 space-y-1 border-l border-gray-200 pl-2 dark:border-gray-700">
|
|
96
|
+
<!-- Child Domains -->
|
|
97
|
+
{#each getChildDomains(domain.id) as child (child.id)}
|
|
98
|
+
<li>
|
|
99
|
+
<a
|
|
100
|
+
href="/domains/{child.id}"
|
|
101
|
+
class="block rounded px-2 py-1 text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
|
|
102
|
+
>
|
|
103
|
+
{child.name}
|
|
104
|
+
</a>
|
|
105
|
+
</li>
|
|
106
|
+
{/each}
|
|
107
|
+
|
|
108
|
+
<!-- Use Cases -->
|
|
109
|
+
{#each getDomainUseCases(domain.id) as useCase (useCase.id)}
|
|
110
|
+
<li>
|
|
111
|
+
<div class="flex items-center gap-1">
|
|
112
|
+
{#if getUseCaseServices(useCase).length > 0}
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onclick={() => {
|
|
116
|
+
toggleUseCase(useCase.id);
|
|
117
|
+
}}
|
|
118
|
+
class="flex h-5 w-5 items-center justify-center rounded text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700"
|
|
119
|
+
aria-expanded={expandedUseCases.has(useCase.id)}
|
|
120
|
+
aria-label={expandedUseCases.has(useCase.id) ? 'Collapse' : 'Expand'}
|
|
121
|
+
>
|
|
122
|
+
<svg
|
|
123
|
+
class="h-3 w-3 transition-transform {expandedUseCases.has(useCase.id)
|
|
124
|
+
? 'rotate-90'
|
|
125
|
+
: ''}"
|
|
126
|
+
fill="currentColor"
|
|
127
|
+
viewBox="0 0 20 20"
|
|
128
|
+
>
|
|
129
|
+
<path
|
|
130
|
+
fill-rule="evenodd"
|
|
131
|
+
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"
|
|
132
|
+
clip-rule="evenodd"
|
|
133
|
+
/>
|
|
134
|
+
</svg>
|
|
135
|
+
</button>
|
|
136
|
+
{:else}
|
|
137
|
+
<span class="w-5"></span>
|
|
138
|
+
{/if}
|
|
139
|
+
<a
|
|
140
|
+
href="/use-cases/{useCase.id}"
|
|
141
|
+
class="flex-1 rounded px-2 py-1 text-primary-600 hover:bg-gray-100 dark:text-primary-400 dark:hover:bg-gray-700"
|
|
142
|
+
>
|
|
143
|
+
{useCase.name}
|
|
144
|
+
</a>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{#if expandedUseCases.has(useCase.id)}
|
|
148
|
+
<ul
|
|
149
|
+
class="ml-6 mt-1 space-y-1 border-l border-gray-200 pl-2 dark:border-gray-700"
|
|
150
|
+
>
|
|
151
|
+
{#each getUseCaseServices(useCase) as service (service.id)}
|
|
152
|
+
<li>
|
|
153
|
+
<a
|
|
154
|
+
href="/services/{service.id}"
|
|
155
|
+
class="block rounded px-2 py-1 text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
156
|
+
>
|
|
157
|
+
{service.name}
|
|
158
|
+
</a>
|
|
159
|
+
</li>
|
|
160
|
+
{/each}
|
|
161
|
+
</ul>
|
|
162
|
+
{/if}
|
|
163
|
+
</li>
|
|
164
|
+
{/each}
|
|
165
|
+
|
|
166
|
+
<!-- Services directly in domain (not via use case) -->
|
|
167
|
+
{#each getDomainServices(domain.id) as service (service.id)}
|
|
168
|
+
<li>
|
|
169
|
+
<a
|
|
170
|
+
href="/services/{service.id}"
|
|
171
|
+
class="ml-5 block rounded px-2 py-1 text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
172
|
+
>
|
|
173
|
+
{service.name}
|
|
174
|
+
</a>
|
|
175
|
+
</li>
|
|
176
|
+
{/each}
|
|
177
|
+
</ul>
|
|
178
|
+
{/if}
|
|
179
|
+
</li>
|
|
180
|
+
{/each}
|
|
181
|
+
</ul>
|
|
182
|
+
</nav>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Domain, UseCase, Service } from '@cwygoda/service-catalog-core/domain';
|
|
2
|
+
interface Props {
|
|
3
|
+
domains: Domain[];
|
|
4
|
+
useCases: UseCase[];
|
|
5
|
+
services: Service[];
|
|
6
|
+
}
|
|
7
|
+
declare const NavTree: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type NavTree = ReturnType<typeof NavTree>;
|
|
9
|
+
export default NavTree;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Service } from '@cwygoda/service-catalog-core/domain';
|
|
3
|
+
|
|
4
|
+
let { service }: { service: Service } = $props();
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<a
|
|
8
|
+
href="/services/{service.id}"
|
|
9
|
+
class="block rounded-lg border border-gray-200 bg-white p-6 shadow-sm transition-shadow hover:shadow-md dark:border-gray-700 dark:bg-gray-800"
|
|
10
|
+
>
|
|
11
|
+
<div class="mb-2 flex items-center justify-between">
|
|
12
|
+
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
|
|
13
|
+
{service.name}
|
|
14
|
+
</h3>
|
|
15
|
+
{#if service.metadata?.version}
|
|
16
|
+
<span
|
|
17
|
+
class="rounded-full bg-primary-100 px-2.5 py-0.5 text-xs font-medium text-primary-800 dark:bg-primary-900 dark:text-primary-200"
|
|
18
|
+
>
|
|
19
|
+
v{service.metadata.version}
|
|
20
|
+
</span>
|
|
21
|
+
{/if}
|
|
22
|
+
</div>
|
|
23
|
+
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
24
|
+
{service.description}
|
|
25
|
+
</p>
|
|
26
|
+
</a>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Service } from '@cwygoda/service-catalog-core/domain';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
service: Service;
|
|
4
|
+
};
|
|
5
|
+
declare const ServiceCard: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type ServiceCard = ReturnType<typeof ServiceCard>;
|
|
7
|
+
export default ServiceCard;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/// <reference types="@testing-library/jest-dom" />
|
|
2
|
+
import { render, screen } from '@testing-library/svelte';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import ServiceCard from './ServiceCard.svelte';
|
|
5
|
+
describe('ServiceCard', () => {
|
|
6
|
+
const baseService = {
|
|
7
|
+
id: 'test-service',
|
|
8
|
+
name: 'Test Service',
|
|
9
|
+
description: 'A test service description',
|
|
10
|
+
};
|
|
11
|
+
it('renders service name', () => {
|
|
12
|
+
render(ServiceCard, { props: { service: baseService } });
|
|
13
|
+
expect(screen.getByText('Test Service')).toBeInTheDocument();
|
|
14
|
+
});
|
|
15
|
+
it('renders service description', () => {
|
|
16
|
+
render(ServiceCard, { props: { service: baseService } });
|
|
17
|
+
expect(screen.getByText('A test service description')).toBeInTheDocument();
|
|
18
|
+
});
|
|
19
|
+
it('links to service detail page', () => {
|
|
20
|
+
render(ServiceCard, { props: { service: baseService } });
|
|
21
|
+
const link = screen.getByRole('link');
|
|
22
|
+
expect(link).toHaveAttribute('href', '/services/test-service');
|
|
23
|
+
});
|
|
24
|
+
it('shows version badge when metadata has version', () => {
|
|
25
|
+
const serviceWithVersion = {
|
|
26
|
+
...baseService,
|
|
27
|
+
metadata: { version: '1.2.3' },
|
|
28
|
+
};
|
|
29
|
+
render(ServiceCard, { props: { service: serviceWithVersion } });
|
|
30
|
+
expect(screen.getByText('v1.2.3')).toBeInTheDocument();
|
|
31
|
+
});
|
|
32
|
+
it('does not show version badge when no metadata', () => {
|
|
33
|
+
render(ServiceCard, { props: { service: baseService } });
|
|
34
|
+
expect(screen.queryByText(/^v\d/)).not.toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
});
|