@industream/flowmaker-flowbox-ui-components 0.0.11 → 0.0.13
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/DCCatalogEntryPicker/DCCatalogEntryPicker.svelte +113 -0
- package/dist/DCCatalogEntryPicker/DCCatalogEntryPicker.svelte.d.ts +15 -0
- package/dist/DCCatalogEntryPicker/SelectedTags.svelte +150 -0
- package/dist/DCCatalogEntryPicker/SelectedTags.svelte.d.ts +18 -0
- package/dist/DCCatalogEntryPicker/TagBrowser.svelte +210 -0
- package/dist/DCCatalogEntryPicker/TagBrowser.svelte.d.ts +24 -0
- package/dist/DCCatalogEntryPicker/picker-column-helpers.d.ts +74 -0
- package/dist/DCCatalogEntryPicker/picker-column-helpers.js +105 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/package.json +10 -1
- package/src/DCCatalogEntryPicker/DCCatalogEntryPicker.svelte +113 -0
- package/src/DCCatalogEntryPicker/SelectedTags.svelte +150 -0
- package/src/DCCatalogEntryPicker/TagBrowser.svelte +210 -0
- package/src/DCCatalogEntryPicker/picker-column-helpers.ts +126 -0
- package/src/index.ts +3 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* DCCatalogEntryPicker — DataCatalog entry picker with configurable columns.
|
|
4
|
+
*
|
|
5
|
+
* Composes TagBrowser (searchable table with multi-select) and SelectedTags
|
|
6
|
+
* (collapsible summary with remove). Owns the DataCatalog fetch lifecycle.
|
|
7
|
+
*
|
|
8
|
+
* Column keys reference the COLUMN_DEFS registry in columns.ts:
|
|
9
|
+
* 'name' → CatalogEntry.name (text)
|
|
10
|
+
* 'dataType' → CatalogEntry.dataType (pill)
|
|
11
|
+
* 'nodeId' → CatalogEntry.sourceParams.nodeId (code)
|
|
12
|
+
* 'unit' → CatalogEntry.metadata.unit (pill)
|
|
13
|
+
* 'labels' → CatalogEntry.labels[] (label pills)
|
|
14
|
+
*
|
|
15
|
+
* @prop dcApiUrl — DataCatalog API base URL
|
|
16
|
+
* @prop sourceConnectionId — Filters catalog entries by source connection
|
|
17
|
+
* @prop selectedIds — (bindable) Array of selected CatalogEntry IDs
|
|
18
|
+
* @prop entries — (bindable) Loaded catalog entries (exposed for parent access)
|
|
19
|
+
* @prop columns — Column keys for the browse table (default: name, dataType, labels)
|
|
20
|
+
* @prop selectedColumnsDisplay — Column keys for the selected tags panel (default: name, dataType, labels)
|
|
21
|
+
* @prop emptyMessage — Message when no entries match the source connection
|
|
22
|
+
* @prop onSelectionChange — Called when selection changes (receives full selectedIds array)
|
|
23
|
+
* @prop onRemove — Called when a single entry is removed from the selection
|
|
24
|
+
*/
|
|
25
|
+
import { DataCatalogClient } from '@industream/datacatalog-client';
|
|
26
|
+
import type { CatalogEntry } from '@industream/datacatalog-client/dto';
|
|
27
|
+
import { DEFAULT_COLUMNS, DEFAULT_SELECTED_COLUMNS_DISPLAY } from './picker-column-helpers';
|
|
28
|
+
import TagBrowser from './TagBrowser.svelte';
|
|
29
|
+
import SelectedTags from './SelectedTags.svelte';
|
|
30
|
+
|
|
31
|
+
interface Props {
|
|
32
|
+
dcApiUrl?: string;
|
|
33
|
+
sourceConnectionId?: string;
|
|
34
|
+
selectedIds?: string[];
|
|
35
|
+
entries?: CatalogEntry[];
|
|
36
|
+
columns?: string[];
|
|
37
|
+
selectedColumnsDisplay?: string[];
|
|
38
|
+
emptyMessage?: string;
|
|
39
|
+
onSelectionChange?: (ids: string[]) => void;
|
|
40
|
+
onRemove?: (id: string) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let {
|
|
44
|
+
dcApiUrl = '',
|
|
45
|
+
sourceConnectionId = '',
|
|
46
|
+
selectedIds = $bindable([]),
|
|
47
|
+
entries = $bindable([]),
|
|
48
|
+
columns = DEFAULT_COLUMNS,
|
|
49
|
+
selectedColumnsDisplay = DEFAULT_SELECTED_COLUMNS_DISPLAY,
|
|
50
|
+
emptyMessage = 'No entries found for this source connection.',
|
|
51
|
+
onSelectionChange = null,
|
|
52
|
+
onRemove = null
|
|
53
|
+
}: Props = $props();
|
|
54
|
+
|
|
55
|
+
let loading = $state(false);
|
|
56
|
+
let error = $state('');
|
|
57
|
+
|
|
58
|
+
$effect(() => {
|
|
59
|
+
if (dcApiUrl && sourceConnectionId) {
|
|
60
|
+
loadEntries(dcApiUrl, sourceConnectionId);
|
|
61
|
+
} else {
|
|
62
|
+
entries = [];
|
|
63
|
+
error = '';
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
async function loadEntries(apiUrl: string, connId: string) {
|
|
68
|
+
loading = true;
|
|
69
|
+
error = '';
|
|
70
|
+
try {
|
|
71
|
+
const client = new DataCatalogClient({ baseUrl: apiUrl });
|
|
72
|
+
const result = await client.catalogEntries.get({ sourceConnectionIds: [connId] });
|
|
73
|
+
entries = result.items ?? [];
|
|
74
|
+
} catch (err: any) {
|
|
75
|
+
error = err.message ?? 'Failed to load catalog entries';
|
|
76
|
+
entries = [];
|
|
77
|
+
} finally {
|
|
78
|
+
loading = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<TagBrowser
|
|
84
|
+
{entries}
|
|
85
|
+
bind:selectedIds
|
|
86
|
+
{columns}
|
|
87
|
+
{loading}
|
|
88
|
+
{error}
|
|
89
|
+
{emptyMessage}
|
|
90
|
+
{onSelectionChange}
|
|
91
|
+
/>
|
|
92
|
+
|
|
93
|
+
{#if selectedIds.length > 0}
|
|
94
|
+
<SelectedTags
|
|
95
|
+
{entries}
|
|
96
|
+
bind:selectedIds
|
|
97
|
+
columns={selectedColumnsDisplay}
|
|
98
|
+
{onRemove}
|
|
99
|
+
/>
|
|
100
|
+
{:else}
|
|
101
|
+
<small class="helper-text warning">Select at least one tag to monitor</small>
|
|
102
|
+
{/if}
|
|
103
|
+
|
|
104
|
+
<style>
|
|
105
|
+
.helper-text {
|
|
106
|
+
color: var(--cds-text-secondary, #525252);
|
|
107
|
+
font-size: 0.75rem;
|
|
108
|
+
margin-top: 0.25rem;
|
|
109
|
+
}
|
|
110
|
+
.helper-text.warning {
|
|
111
|
+
color: var(--cds-support-warning, #f1c21b);
|
|
112
|
+
}
|
|
113
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CatalogEntry } from '@industream/datacatalog-client/dto';
|
|
2
|
+
interface Props {
|
|
3
|
+
dcApiUrl?: string;
|
|
4
|
+
sourceConnectionId?: string;
|
|
5
|
+
selectedIds?: string[];
|
|
6
|
+
entries?: CatalogEntry[];
|
|
7
|
+
columns?: string[];
|
|
8
|
+
selectedColumnsDisplay?: string[];
|
|
9
|
+
emptyMessage?: string;
|
|
10
|
+
onSelectionChange?: (ids: string[]) => void;
|
|
11
|
+
onRemove?: (id: string) => void;
|
|
12
|
+
}
|
|
13
|
+
declare const DCCatalogEntryPicker: import("svelte").Component<Props, {}, "entries" | "selectedIds">;
|
|
14
|
+
type DCCatalogEntryPicker = ReturnType<typeof DCCatalogEntryPicker>;
|
|
15
|
+
export default DCCatalogEntryPicker;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { DEFAULT_SELECTED_COLUMNS_DISPLAY, resolveColumn, resolveValue } from './picker-column-helpers';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
entries = [],
|
|
6
|
+
selectedIds = $bindable([]),
|
|
7
|
+
columns = DEFAULT_SELECTED_COLUMNS_DISPLAY,
|
|
8
|
+
onRemove = null
|
|
9
|
+
} = $props();
|
|
10
|
+
|
|
11
|
+
let expanded = $state(true);
|
|
12
|
+
|
|
13
|
+
/** Resolved column descriptors — maps each column key to its label, resolver and render type */
|
|
14
|
+
const columnDescriptors = $derived(
|
|
15
|
+
columns
|
|
16
|
+
.map(k => { const resolved = resolveColumn(k); return resolved ? { key: k, ...resolved } : null; })
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const selectedEntries = $derived(() => {
|
|
21
|
+
const idSet = new Set(selectedIds);
|
|
22
|
+
return entries.filter(e => idSet.has(e.id));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const selectedCount = $derived(selectedIds.length);
|
|
26
|
+
|
|
27
|
+
$effect(() => {
|
|
28
|
+
if (selectedCount === 0) {
|
|
29
|
+
expanded = false;
|
|
30
|
+
} else if (selectedCount > 0 && !expanded) {
|
|
31
|
+
expanded = true;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function handleRemove(id) {
|
|
36
|
+
onRemove?.(id);
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div class="selected-tags">
|
|
41
|
+
<button class="panel-header" aria-expanded={expanded} onclick={() => expanded = !expanded}>
|
|
42
|
+
<svg class="chevron" class:chevron-expanded={expanded} viewBox="0 0 16 16" width="16" height="16">
|
|
43
|
+
<path d="M11 8L6 13V3z" fill="currentColor"></path>
|
|
44
|
+
</svg>
|
|
45
|
+
<span class="panel-title">Selected Tags ({selectedCount})</span>
|
|
46
|
+
</button>
|
|
47
|
+
|
|
48
|
+
{#if expanded}
|
|
49
|
+
<div class="panel-body">
|
|
50
|
+
{#if selectedEntries().length === 0}
|
|
51
|
+
<div class="empty-message">No tags selected</div>
|
|
52
|
+
{:else}
|
|
53
|
+
<div class="tag-list">
|
|
54
|
+
{#each selectedEntries() as entry (entry.id)}
|
|
55
|
+
<div class="tag-row">
|
|
56
|
+
<div class="tag-info">
|
|
57
|
+
{#each columnDescriptors as columnDescriptor, index (columnDescriptor.key)}
|
|
58
|
+
{@const val = resolveValue(entry, columnDescriptor.key)}
|
|
59
|
+
{#if index === 0}
|
|
60
|
+
<!-- First column: bold primary label -->
|
|
61
|
+
<span class="tag-primary">{val ?? '-'}</span>
|
|
62
|
+
{:else if columnDescriptor.type === 'labels' && Array.isArray(val)}
|
|
63
|
+
{#each val as label (label.id)}
|
|
64
|
+
<span class="meta-item">{label.name}</span>
|
|
65
|
+
{/each}
|
|
66
|
+
{:else if columnDescriptor.type === 'code'}
|
|
67
|
+
{#if val}<code class="tag-code">{val}</code>{/if}
|
|
68
|
+
{:else}
|
|
69
|
+
{#if val}<span class="meta-item">{val}</span>{/if}
|
|
70
|
+
{/if}
|
|
71
|
+
{/each}
|
|
72
|
+
</div>
|
|
73
|
+
<button
|
|
74
|
+
class="remove-button"
|
|
75
|
+
title="Remove {entry.name}"
|
|
76
|
+
aria-label="Remove {entry.name}"
|
|
77
|
+
onclick={() => handleRemove(entry.id)}
|
|
78
|
+
>
|
|
79
|
+
<svg viewBox="0 0 16 16" width="14" height="14"><path d="M12 4.7L11.3 4 8 7.3 4.7 4 4 4.7 7.3 8 4 11.3l.7.7L8 8.7l3.3 3.3.7-.7L8.7 8z" fill="currentColor"></path></svg>
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
{/each}
|
|
83
|
+
</div>
|
|
84
|
+
{/if}
|
|
85
|
+
</div>
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<style>
|
|
90
|
+
.selected-tags {
|
|
91
|
+
border: 1px solid var(--cds-border-subtle-01, #393939);
|
|
92
|
+
background: var(--cds-layer-01, #262626);
|
|
93
|
+
}
|
|
94
|
+
.panel-header {
|
|
95
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
96
|
+
width: 100%; padding: 0.5rem 0.75rem;
|
|
97
|
+
background: var(--cds-layer-02, #2e2e2e); border: none;
|
|
98
|
+
color: var(--cds-text-primary, #f4f4f4);
|
|
99
|
+
font-size: 0.8125rem; font-weight: 600;
|
|
100
|
+
cursor: pointer; text-align: left; transition: background-color 0.15s;
|
|
101
|
+
}
|
|
102
|
+
.panel-header:hover { background: var(--cds-layer-hover-01, #4c4c4c); }
|
|
103
|
+
.chevron {
|
|
104
|
+
color: var(--cds-text-secondary, #c6c6c6);
|
|
105
|
+
transition: transform 0.15s ease; flex-shrink: 0;
|
|
106
|
+
}
|
|
107
|
+
.chevron-expanded { transform: rotate(90deg); }
|
|
108
|
+
.panel-title { flex: 1; }
|
|
109
|
+
.panel-body { max-height: 200px; overflow-y: auto; }
|
|
110
|
+
.empty-message {
|
|
111
|
+
padding: 1rem; text-align: center;
|
|
112
|
+
color: var(--cds-text-helper, #8d8d8d); font-size: 0.8125rem;
|
|
113
|
+
}
|
|
114
|
+
.tag-row {
|
|
115
|
+
display: flex; align-items: center; gap: 0.5rem;
|
|
116
|
+
padding: 0.375rem 0.75rem;
|
|
117
|
+
border-bottom: 1px solid var(--cds-border-subtle-01, #393939);
|
|
118
|
+
transition: background-color 0.1s;
|
|
119
|
+
}
|
|
120
|
+
.tag-row:last-child { border-bottom: none; }
|
|
121
|
+
.tag-row:hover { background: var(--cds-layer-hover-01, #4c4c4c); }
|
|
122
|
+
.tag-info {
|
|
123
|
+
flex: 1; display: flex; align-items: center; gap: 0.75rem;
|
|
124
|
+
min-width: 0; overflow: hidden;
|
|
125
|
+
}
|
|
126
|
+
.tag-primary {
|
|
127
|
+
font-weight: 600; font-size: 0.8125rem;
|
|
128
|
+
color: var(--cds-text-primary, #f4f4f4);
|
|
129
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
130
|
+
flex-shrink: 0; max-width: 12rem;
|
|
131
|
+
}
|
|
132
|
+
.tag-code {
|
|
133
|
+
font-family: 'IBM Plex Mono', monospace; font-size: 0.6875rem;
|
|
134
|
+
color: var(--cds-text-secondary, #c6c6c6);
|
|
135
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0;
|
|
136
|
+
}
|
|
137
|
+
.meta-item {
|
|
138
|
+
padding: 0.0625rem 0.375rem;
|
|
139
|
+
background: var(--cds-tag-background-gray, #393939);
|
|
140
|
+
color: var(--cds-tag-color-gray, #c6c6c6);
|
|
141
|
+
font-size: 0.6875rem; border-radius: 1rem;
|
|
142
|
+
}
|
|
143
|
+
.remove-button {
|
|
144
|
+
background: transparent; border: none;
|
|
145
|
+
color: var(--cds-text-secondary, #c6c6c6);
|
|
146
|
+
cursor: pointer; padding: 0.25rem;
|
|
147
|
+
transition: color 0.15s; flex-shrink: 0;
|
|
148
|
+
}
|
|
149
|
+
.remove-button:hover { color: var(--cds-support-error, #fa4d56); }
|
|
150
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default SelectedTags;
|
|
2
|
+
type SelectedTags = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const SelectedTags: import("svelte").Component<{
|
|
7
|
+
entries?: any[];
|
|
8
|
+
selectedIds?: any[];
|
|
9
|
+
columns?: typeof DEFAULT_SELECTED_COLUMNS_DISPLAY;
|
|
10
|
+
onRemove?: any;
|
|
11
|
+
}, {}, "selectedIds">;
|
|
12
|
+
type $$ComponentProps = {
|
|
13
|
+
entries?: any[];
|
|
14
|
+
selectedIds?: any[];
|
|
15
|
+
columns?: typeof DEFAULT_SELECTED_COLUMNS_DISPLAY;
|
|
16
|
+
onRemove?: any;
|
|
17
|
+
};
|
|
18
|
+
import { DEFAULT_SELECTED_COLUMNS_DISPLAY } from './picker-column-helpers';
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { DEFAULT_COLUMNS, resolveColumn, resolveValue, resolveSearchText } from './picker-column-helpers';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
entries = [],
|
|
6
|
+
selectedIds = $bindable([]),
|
|
7
|
+
columns = DEFAULT_COLUMNS,
|
|
8
|
+
loading = false,
|
|
9
|
+
error = '',
|
|
10
|
+
emptyMessage = 'No entries found for this source connection.',
|
|
11
|
+
onSelectionChange = null
|
|
12
|
+
} = $props();
|
|
13
|
+
|
|
14
|
+
let searchQuery = $state('');
|
|
15
|
+
|
|
16
|
+
/** Resolved column descriptors for rendering — maps each column key to its label, resolver and render type */
|
|
17
|
+
const columnDescriptors = $derived(
|
|
18
|
+
columns
|
|
19
|
+
.map(k => { const col = resolveColumn(k); return col ? { key: k, ...col } : null; })
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const filtered = $derived(() => {
|
|
24
|
+
if (!searchQuery.trim()) return entries;
|
|
25
|
+
const q = searchQuery.toLowerCase();
|
|
26
|
+
return entries.filter(e => resolveSearchText(e, columns).includes(q));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const selectedCount = $derived(selectedIds.length);
|
|
30
|
+
const totalCount = $derived(entries.length);
|
|
31
|
+
const allVisibleSelected = $derived(filtered().length > 0 && filtered().every(e => selectedIds.includes(e.id)));
|
|
32
|
+
|
|
33
|
+
function toggleEntry(id) {
|
|
34
|
+
selectedIds = selectedIds.indexOf(id) >= 0
|
|
35
|
+
? selectedIds.filter(s => s !== id)
|
|
36
|
+
: [...selectedIds, id];
|
|
37
|
+
onSelectionChange?.(selectedIds);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function selectAll() {
|
|
41
|
+
const merged = new Set(selectedIds);
|
|
42
|
+
filtered().forEach(e => merged.add(e.id));
|
|
43
|
+
selectedIds = [...merged];
|
|
44
|
+
onSelectionChange?.(selectedIds);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function deselectAll() {
|
|
48
|
+
const visibleSet = new Set(filtered().map(e => e.id));
|
|
49
|
+
selectedIds = selectedIds.filter(id => !visibleSet.has(id));
|
|
50
|
+
onSelectionChange?.(selectedIds);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toggleAll() {
|
|
54
|
+
allVisibleSelected ? deselectAll() : selectAll();
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<div class="tag-browser">
|
|
59
|
+
<div class="toolbar">
|
|
60
|
+
<div class="search-wrapper">
|
|
61
|
+
<svg class="search-icon" viewBox="0 0 16 16" width="16" height="16">
|
|
62
|
+
<path d="M15.5 13.586L11.742 9.828A5.514 5.514 0 0 0 13 6.5 5.506 5.506 0 0 0 7.5 1 5.506 5.506 0 0 0 2 6.5 5.506 5.506 0 0 0 7.5 12a5.514 5.514 0 0 0 3.328-1.258l3.758 3.758zM3 6.5A4.505 4.505 0 0 1 7.5 2 4.505 4.505 0 0 1 12 6.5 4.505 4.505 0 0 1 7.5 11 4.505 4.505 0 0 1 3 6.5z" fill="currentColor"></path>
|
|
63
|
+
</svg>
|
|
64
|
+
<input type="text" class="search-input" placeholder="Search..." bind:value={searchQuery} />
|
|
65
|
+
</div>
|
|
66
|
+
<div class="toolbar-actions">
|
|
67
|
+
<span class="count-indicator">{selectedCount} / {totalCount} selected</span>
|
|
68
|
+
<button class="action-button" onclick={selectAll} disabled={loading || filtered().length === 0}>Select All</button>
|
|
69
|
+
<button class="action-button" onclick={deselectAll} disabled={loading || selectedCount === 0}>Deselect All</button>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
{#if loading}
|
|
74
|
+
<div class="state-message"><div class="spinner"></div> <span>Loading catalog entries...</span></div>
|
|
75
|
+
{:else if error}
|
|
76
|
+
<div class="state-message error-state">
|
|
77
|
+
<svg viewBox="0 0 16 16" width="16" height="16"><path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm-.75 3.5h1.5v5h-1.5zm.75 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2z" fill="currentColor"></path></svg>
|
|
78
|
+
<span>{error}</span>
|
|
79
|
+
</div>
|
|
80
|
+
{:else if entries.length === 0}
|
|
81
|
+
<div class="state-message empty-state"><span>{emptyMessage}</span></div>
|
|
82
|
+
{:else}
|
|
83
|
+
<div class="table-container">
|
|
84
|
+
<table class="entry-table">
|
|
85
|
+
<!-- Table headers: one checkbox column + one column per columnDescriptor -->
|
|
86
|
+
<thead>
|
|
87
|
+
<tr>
|
|
88
|
+
<th class="col-checkbox"><input type="checkbox" title="Toggle all visible" checked={allVisibleSelected} onchange={toggleAll} /></th>
|
|
89
|
+
{#each columnDescriptors as columnDescriptor (columnDescriptor.key)}
|
|
90
|
+
<th>{columnDescriptor.label}</th>
|
|
91
|
+
{/each}
|
|
92
|
+
</tr>
|
|
93
|
+
</thead>
|
|
94
|
+
|
|
95
|
+
<!-- Table body: one row per filtered catalog entry, cells rendered by column type -->
|
|
96
|
+
<tbody>
|
|
97
|
+
{#each filtered() as entry (entry.id)}
|
|
98
|
+
{@const isSelected = selectedIds.includes(entry.id)}
|
|
99
|
+
<tr class:selected={isSelected} onclick={() => toggleEntry(entry.id)}>
|
|
100
|
+
<td class="col-checkbox"><input type="checkbox" checked={isSelected} onclick={(e) => e.stopPropagation()} onchange={() => toggleEntry(entry.id)} /></td>
|
|
101
|
+
|
|
102
|
+
<!-- Render each column cell according to its type (text, code, pill, labels) -->
|
|
103
|
+
{#each columnDescriptors as columnDescriptor (columnDescriptor.key)}
|
|
104
|
+
{@const val = resolveValue(entry, columnDescriptor.key)}
|
|
105
|
+
<td title={columnDescriptor.type === 'text' ? (val ?? '') : ''}>
|
|
106
|
+
{#if columnDescriptor.type === 'labels' && Array.isArray(val)}
|
|
107
|
+
{#each val as label (label.id)}
|
|
108
|
+
<span class="label-pill">{label.name}</span>
|
|
109
|
+
{/each}
|
|
110
|
+
{:else if columnDescriptor.type === 'code'}
|
|
111
|
+
<code>{val ?? '-'}</code>
|
|
112
|
+
{:else if columnDescriptor.type === 'pill'}
|
|
113
|
+
{#if val}<span class="pill">{val}</span>{:else}-{/if}
|
|
114
|
+
{:else}
|
|
115
|
+
{val ?? '-'}
|
|
116
|
+
{/if}
|
|
117
|
+
</td>
|
|
118
|
+
{/each}
|
|
119
|
+
</tr>
|
|
120
|
+
{/each}
|
|
121
|
+
</tbody>
|
|
122
|
+
</table>
|
|
123
|
+
</div>
|
|
124
|
+
{#if filtered().length !== entries.length}
|
|
125
|
+
<div class="filter-info">Showing {filtered().length} of {entries.length} entries</div>
|
|
126
|
+
{/if}
|
|
127
|
+
{/if}
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<style>
|
|
131
|
+
.tag-browser { gap: 0.5rem; }
|
|
132
|
+
.toolbar { gap: 0.5rem; }
|
|
133
|
+
.search-wrapper { position: relative; display: flex; align-items: center; }
|
|
134
|
+
.search-icon { position: absolute; left: 0.75rem; color: var(--cds-text-secondary, #525252); pointer-events: none; }
|
|
135
|
+
.search-input {
|
|
136
|
+
width: 100%; padding: 0.5rem 0.75rem 0.5rem 2.25rem;
|
|
137
|
+
background: var(--cds-field-01, #353535); border: none;
|
|
138
|
+
border-bottom: 1px solid var(--cds-border-strong-01, #6f6f6f);
|
|
139
|
+
color: var(--cds-text-primary, #f4f4f4); font-size: 0.875rem;
|
|
140
|
+
outline: none; transition: border-color 0.15s;
|
|
141
|
+
}
|
|
142
|
+
.search-input::placeholder { color: var(--cds-text-placeholder, #6f6f6f); }
|
|
143
|
+
.search-input:focus { border-bottom-color: var(--cds-focus, #0f62fe); }
|
|
144
|
+
.toolbar-actions { display: flex; align-items: center; gap: 0.5rem; }
|
|
145
|
+
.count-indicator { font-size: 0.75rem; color: var(--cds-text-secondary, #c6c6c6); margin-right: auto; }
|
|
146
|
+
.action-button {
|
|
147
|
+
padding: 0.25rem 0.75rem; background: transparent;
|
|
148
|
+
border: 1px solid var(--cds-border-strong-01, #6f6f6f);
|
|
149
|
+
color: var(--cds-text-primary, #f4f4f4); font-size: 0.75rem;
|
|
150
|
+
cursor: pointer; transition: background-color 0.15s;
|
|
151
|
+
}
|
|
152
|
+
.action-button:hover:not(:disabled) { background: var(--cds-layer-hover-01, #4c4c4c); }
|
|
153
|
+
.action-button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
154
|
+
.state-message {
|
|
155
|
+
display: flex; align-items: center; justify-content: center;
|
|
156
|
+
gap: 0.5rem; padding: 2rem;
|
|
157
|
+
color: var(--cds-text-secondary, #c6c6c6); font-size: 0.875rem;
|
|
158
|
+
}
|
|
159
|
+
.error-state { color: var(--cds-support-error, #fa4d56); }
|
|
160
|
+
.empty-state { color: var(--cds-text-helper, #8d8d8d); }
|
|
161
|
+
.spinner {
|
|
162
|
+
width: 1rem; height: 1rem;
|
|
163
|
+
border: 2px solid var(--cds-border-subtle-01, #e0e0e0);
|
|
164
|
+
border-top-color: var(--cds-interactive, #0f62fe);
|
|
165
|
+
border-radius: 50%; animation: spin 0.8s linear infinite;
|
|
166
|
+
}
|
|
167
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
168
|
+
.table-container {
|
|
169
|
+
max-height: 400px; overflow-y: auto;
|
|
170
|
+
border: 1px solid var(--cds-border-subtle-01, #393939);
|
|
171
|
+
}
|
|
172
|
+
.entry-table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
|
|
173
|
+
.entry-table thead { position: sticky; top: 0; z-index: 1; }
|
|
174
|
+
.entry-table th {
|
|
175
|
+
background: var(--cds-layer-02, #262626);
|
|
176
|
+
color: var(--cds-text-secondary, #c6c6c6);
|
|
177
|
+
font-weight: 600; font-size: 0.75rem; text-transform: uppercase;
|
|
178
|
+
letter-spacing: 0.02em; padding: 0.5rem;
|
|
179
|
+
text-align: left; border-bottom: 1px solid var(--cds-border-subtle-01, #393939);
|
|
180
|
+
white-space: nowrap;
|
|
181
|
+
}
|
|
182
|
+
.entry-table td {
|
|
183
|
+
padding: 0.375rem 0.5rem;
|
|
184
|
+
border-bottom: 1px solid var(--cds-border-subtle-01, #393939);
|
|
185
|
+
color: var(--cds-text-primary, #f4f4f4);
|
|
186
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
187
|
+
}
|
|
188
|
+
.entry-table tbody tr { cursor: pointer; transition: background-color 0.1s; }
|
|
189
|
+
.entry-table tbody tr:nth-child(even) { background: var(--cds-layer-01, #262626); }
|
|
190
|
+
.entry-table tbody tr:nth-child(odd) { background: var(--cds-layer-02, #2e2e2e); }
|
|
191
|
+
.entry-table tbody tr:hover { background: var(--cds-layer-hover-01, #4c4c4c); }
|
|
192
|
+
.entry-table tbody tr.selected { background: var(--cds-layer-selected-01, #3d3d3d); }
|
|
193
|
+
.entry-table tbody tr.selected:hover { background: var(--cds-layer-selected-hover-01, #4c4c4c); }
|
|
194
|
+
.col-checkbox { width: 2rem; text-align: center; }
|
|
195
|
+
.col-checkbox input[type="checkbox"] { cursor: pointer; accent-color: var(--cds-interactive, #0f62fe); }
|
|
196
|
+
code {
|
|
197
|
+
font-family: 'IBM Plex Mono', monospace; font-size: 0.75rem;
|
|
198
|
+
color: var(--cds-text-secondary, #c6c6c6);
|
|
199
|
+
}
|
|
200
|
+
.pill, .label-pill {
|
|
201
|
+
display: inline-block; padding: 0.0625rem 0.375rem;
|
|
202
|
+
background: var(--cds-tag-background-gray, #393939);
|
|
203
|
+
color: var(--cds-tag-color-gray, #c6c6c6);
|
|
204
|
+
font-size: 0.6875rem; border-radius: 1rem; margin-right: 0.25rem;
|
|
205
|
+
}
|
|
206
|
+
.filter-info {
|
|
207
|
+
font-size: 0.75rem; color: var(--cds-text-helper, #8d8d8d);
|
|
208
|
+
text-align: center; padding: 0.25rem;
|
|
209
|
+
}
|
|
210
|
+
</style>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export default TagBrowser;
|
|
2
|
+
type TagBrowser = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
declare const TagBrowser: import("svelte").Component<{
|
|
7
|
+
entries?: any[];
|
|
8
|
+
selectedIds?: any[];
|
|
9
|
+
columns?: typeof DEFAULT_COLUMNS;
|
|
10
|
+
loading?: boolean;
|
|
11
|
+
error?: string;
|
|
12
|
+
emptyMessage?: string;
|
|
13
|
+
onSelectionChange?: any;
|
|
14
|
+
}, {}, "selectedIds">;
|
|
15
|
+
type $$ComponentProps = {
|
|
16
|
+
entries?: any[];
|
|
17
|
+
selectedIds?: any[];
|
|
18
|
+
columns?: typeof DEFAULT_COLUMNS;
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
error?: string;
|
|
21
|
+
emptyMessage?: string;
|
|
22
|
+
onSelectionChange?: any;
|
|
23
|
+
};
|
|
24
|
+
import { DEFAULT_COLUMNS } from './picker-column-helpers';
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog entry column system.
|
|
3
|
+
*
|
|
4
|
+
* Declarative column definitions that drive both the TagBrowser table
|
|
5
|
+
* and the SelectedTags panel. Consumers pass an array of column keys
|
|
6
|
+
* (e.g. ['name', 'sourceParams.nodeId', 'dataType']) to control display.
|
|
7
|
+
*
|
|
8
|
+
* ── Static column keys ──────────────────────────────────────────────
|
|
9
|
+
*
|
|
10
|
+
* Key | Label | Source | Render
|
|
11
|
+
* -----------|-------------|-----------------------|--------
|
|
12
|
+
* 'name' | Name | CatalogEntry.name | text
|
|
13
|
+
* 'dataType' | Data Type | CatalogEntry.dataType | pill
|
|
14
|
+
* 'labels' | Labels | CatalogEntry.labels[] | labels
|
|
15
|
+
*
|
|
16
|
+
* ── Dynamic column keys (dot-path) ──────────────────────────────────
|
|
17
|
+
*
|
|
18
|
+
* Any key starting with 'sourceParams.' or 'metadata.' resolves the
|
|
19
|
+
* corresponding sub-property at runtime. No pre-registration needed.
|
|
20
|
+
*
|
|
21
|
+
* Key | Label | Source | Render
|
|
22
|
+
* -------------------------|------------|----------------------------------|--------
|
|
23
|
+
* 'sourceParams.nodeId' | Node ID | CatalogEntry.sourceParams.nodeId | code
|
|
24
|
+
* 'sourceParams.topic' | Topic | CatalogEntry.sourceParams.topic | code
|
|
25
|
+
* 'metadata.unit' | Unit | CatalogEntry.metadata.unit | pill
|
|
26
|
+
* 'metadata.description' | Description| CatalogEntry.metadata.description | text
|
|
27
|
+
* ... any other sub-key | (auto) | ... | (auto)
|
|
28
|
+
*
|
|
29
|
+
* Render types:
|
|
30
|
+
* - 'text' : plain text cell (truncated, with title tooltip)
|
|
31
|
+
* - 'code' : monospace <code> cell
|
|
32
|
+
* - 'pill' : small badge/pill (only rendered when value is truthy)
|
|
33
|
+
* - 'labels' : iterates Label[] array, renders each as a pill
|
|
34
|
+
*
|
|
35
|
+
* sourceParams.* keys default to 'code' rendering.
|
|
36
|
+
* metadata.* keys default to 'pill' rendering.
|
|
37
|
+
* Override by adding an explicit entry in COLUMN_DEFS.
|
|
38
|
+
*/
|
|
39
|
+
import type { CatalogEntry } from '@industream/datacatalog-client/dto';
|
|
40
|
+
/** How a cell should be rendered */
|
|
41
|
+
export type ColumnType = 'text' | 'code' | 'pill' | 'labels';
|
|
42
|
+
export interface ColumnDef {
|
|
43
|
+
label: string;
|
|
44
|
+
resolve: (entry: CatalogEntry) => unknown;
|
|
45
|
+
type: ColumnType;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Static column definitions for top-level CatalogEntry fields.
|
|
49
|
+
* For sourceParams / metadata sub-properties, use dot-path keys instead
|
|
50
|
+
* (see {@link resolveColumn}).
|
|
51
|
+
*/
|
|
52
|
+
export declare const COLUMN_DEFS: Record<string, ColumnDef>;
|
|
53
|
+
/** Default columns for the tag browser table */
|
|
54
|
+
export declare const DEFAULT_COLUMNS: string[];
|
|
55
|
+
/** Default columns for the selected tags display panel */
|
|
56
|
+
export declare const DEFAULT_SELECTED_COLUMNS_DISPLAY: string[];
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a column definition for any key — static or dot-path.
|
|
59
|
+
*
|
|
60
|
+
* Static keys are looked up in COLUMN_DEFS.
|
|
61
|
+
* Dot-path keys (sourceParams.*, metadata.*) are resolved dynamically:
|
|
62
|
+
* - sourceParams.* → renders as 'code'
|
|
63
|
+
* - metadata.* → renders as 'pill'
|
|
64
|
+
*/
|
|
65
|
+
export declare const resolveColumn: (key: string) => ColumnDef | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a single column value from a catalog entry.
|
|
68
|
+
*/
|
|
69
|
+
export declare const resolveValue: (entry: CatalogEntry, key: string) => unknown;
|
|
70
|
+
/**
|
|
71
|
+
* Build a lowercase search string from all active column values.
|
|
72
|
+
* Used by TagBrowser's search filter.
|
|
73
|
+
*/
|
|
74
|
+
export declare const resolveSearchText: (entry: CatalogEntry, keys: string[]) => string;
|