@sukramj/fabric-ui 0.1.0 → 0.1.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/package.json +1 -1
- package/src/lib/api/client.ts +1 -0
- package/src/lib/api/schema.gen.ts +17 -0
- package/src/lib/i18n/de.json +3 -0
- package/src/lib/i18n/en.json +3 -0
- package/src/lib/pages/node/HostWarnings.svelte +41 -0
- package/src/lib/pages/node/Selection.svelte +27 -8
- package/src/lib/pages/node/Status.svelte +5 -1
- package/src/lib/pages/node/fields.ts +32 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sukramj/fabric-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "The web interface of go-fabric nodes: Svelte 5 pages, UI kit, design tokens with a Home Assistant theme bridge, and the generated API client.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SukramJ",
|
package/src/lib/api/client.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { components } from "./schema.gen";
|
|
|
8
8
|
type S = components["schemas"];
|
|
9
9
|
export type Problem = S["Problem"];
|
|
10
10
|
export type Status = S["Status"];
|
|
11
|
+
export type Warning = S["Warning"];
|
|
11
12
|
export type Fabric = S["Fabric"];
|
|
12
13
|
export type FabricList = S["FabricList"];
|
|
13
14
|
export type Pairing = S["Pairing"];
|
|
@@ -501,6 +501,19 @@ export interface components {
|
|
|
501
501
|
exposure: boolean;
|
|
502
502
|
/** @description The admin token of token mode; absent when the host decides admin rights. */
|
|
503
503
|
csrf_token?: string;
|
|
504
|
+
/** @description The host's findings about its own configuration; absent when the host reports none (no Options.Warnings), empty when it finds nothing. */
|
|
505
|
+
warnings?: components["schemas"]["Warning"][];
|
|
506
|
+
};
|
|
507
|
+
Warning: {
|
|
508
|
+
/** @description The kind of finding, stable across host releases. */
|
|
509
|
+
code: string;
|
|
510
|
+
/** @description What is wrong, in the host's language. */
|
|
511
|
+
message: string;
|
|
512
|
+
/**
|
|
513
|
+
* Format: date-time
|
|
514
|
+
* @description When the host first saw it; absent when unknown.
|
|
515
|
+
*/
|
|
516
|
+
since?: string;
|
|
504
517
|
};
|
|
505
518
|
Fabric: {
|
|
506
519
|
fabric_index: number;
|
|
@@ -653,6 +666,8 @@ export interface components {
|
|
|
653
666
|
label: string;
|
|
654
667
|
/** @description Offer a value filter built from the listed values. */
|
|
655
668
|
filter: boolean;
|
|
669
|
+
/** @description Default false. The field is a comma-separated list: shown as chips, filtered by membership, the filter's options built from the individual values. */
|
|
670
|
+
multi?: boolean;
|
|
656
671
|
};
|
|
657
672
|
FilterOption: {
|
|
658
673
|
value: string;
|
|
@@ -662,6 +677,8 @@ export interface components {
|
|
|
662
677
|
field: string;
|
|
663
678
|
label: string;
|
|
664
679
|
options: components["schemas"]["FilterOption"][];
|
|
680
|
+
/** @description Default false. Match by membership in the field's comma-separated list (values trimmed; an empty value never matches) instead of equality. */
|
|
681
|
+
multi?: boolean;
|
|
665
682
|
};
|
|
666
683
|
ExposureSchema: {
|
|
667
684
|
columns: components["schemas"]["Column"][];
|
package/src/lib/i18n/de.json
CHANGED
|
@@ -152,6 +152,9 @@
|
|
|
152
152
|
"node.tab.selection": "Auswahl",
|
|
153
153
|
"node.tab.status": "Status",
|
|
154
154
|
"node.title": "Knoten",
|
|
155
|
+
"node.warnings.since": "seit {time}",
|
|
156
|
+
"node.warnings.summary": "{count} Konfigurationswarnungen",
|
|
157
|
+
"node.warnings.title": "Konfigurationswarnungen",
|
|
155
158
|
"select.placeholder": "Auswählen…",
|
|
156
159
|
"shell.language": "Sprache"
|
|
157
160
|
}
|
package/src/lib/i18n/en.json
CHANGED
|
@@ -152,6 +152,9 @@
|
|
|
152
152
|
"node.tab.selection": "Selection",
|
|
153
153
|
"node.tab.status": "Status",
|
|
154
154
|
"node.title": "Node",
|
|
155
|
+
"node.warnings.since": "since {time}",
|
|
156
|
+
"node.warnings.summary": "{count} configuration warnings",
|
|
157
|
+
"node.warnings.title": "Configuration warnings",
|
|
155
158
|
"select.placeholder": "Select…",
|
|
156
159
|
"shell.language": "Language"
|
|
157
160
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!-- The host's findings about its own configuration (Status.warnings):
|
|
2
|
+
a card on the status page, a collapsed list above the selection. -->
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { useFabricUI } from "../../context";
|
|
5
|
+
import type { Warning } from "../../api/client";
|
|
6
|
+
import Badge from "../../kit/Badge.svelte";
|
|
7
|
+
import Card from "../../kit/Card.svelte";
|
|
8
|
+
|
|
9
|
+
type Props = { warnings: Warning[]; collapsed?: boolean };
|
|
10
|
+
let { warnings, collapsed = false }: Props = $props();
|
|
11
|
+
|
|
12
|
+
const { t } = useFabricUI();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
{#snippet list()}
|
|
16
|
+
<ul class="space-y-2">
|
|
17
|
+
{#each warnings as w, i (i)}
|
|
18
|
+
<li class="flex flex-wrap items-center gap-2 text-sm">
|
|
19
|
+
<Badge variant="warning">{w.code}</Badge>
|
|
20
|
+
<span class="text-[var(--ha-primary-text-color)]">{w.message}</span>
|
|
21
|
+
{#if w.since}
|
|
22
|
+
<span class="text-xs text-[var(--ha-secondary-text-color)]">{t("node.warnings.since", { time: new Date(w.since).toLocaleString() })}</span>
|
|
23
|
+
{/if}
|
|
24
|
+
</li>
|
|
25
|
+
{/each}
|
|
26
|
+
</ul>
|
|
27
|
+
{/snippet}
|
|
28
|
+
|
|
29
|
+
{#if warnings.length > 0}
|
|
30
|
+
{#if collapsed}
|
|
31
|
+
<details class="mb-3 rounded-md border border-[var(--ha-divider-color)] px-3 py-2" data-testid="host-warnings">
|
|
32
|
+
<summary class="cursor-pointer text-sm font-medium text-[var(--ha-warning-color)]">{t("node.warnings.summary", { count: warnings.length })}</summary>
|
|
33
|
+
<div class="mt-2">{@render list()}</div>
|
|
34
|
+
</details>
|
|
35
|
+
{:else}
|
|
36
|
+
<Card class="mb-4 p-4" data-testid="host-warnings">
|
|
37
|
+
<h3 class="mb-2 font-medium text-[var(--ha-primary-text-color)]">{t("node.warnings.title")}</h3>
|
|
38
|
+
{@render list()}
|
|
39
|
+
</Card>
|
|
40
|
+
{/if}
|
|
41
|
+
{/if}
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
assets/ui/src/routes/matter/MatterExposureList.svelte:1-584 at
|
|
6
6
|
b40b289a: the host's fixed columns, kind filter and per-item conflict
|
|
7
7
|
hints become the schema's columns and filters; the class chips become a
|
|
8
|
-
value filter per filterable column; Preview before Apply is new
|
|
8
|
+
value filter per filterable column; Preview before Apply is new, and so
|
|
9
|
+
are list-valued fields (Column.multi, Filter.multi) and the host's
|
|
10
|
+
warnings above the list. -->
|
|
9
11
|
<script lang="ts">
|
|
10
12
|
import { onMount } from "svelte";
|
|
11
13
|
import { useFabricUI } from "../../context";
|
|
@@ -24,6 +26,8 @@
|
|
|
24
26
|
import Input from "../../kit/Input.svelte";
|
|
25
27
|
import LoadingState from "../../kit/LoadingState.svelte";
|
|
26
28
|
import Select from "../../kit/Select.svelte";
|
|
29
|
+
import HostWarnings from "./HostWarnings.svelte";
|
|
30
|
+
import { distinctValues, fieldMatches, listValues } from "./fields";
|
|
27
31
|
|
|
28
32
|
const { t, toast, node } = useFabricUI();
|
|
29
33
|
|
|
@@ -32,6 +36,9 @@
|
|
|
32
36
|
const unavailable = $derived(node.status !== null && !node.status.exposure);
|
|
33
37
|
|
|
34
38
|
onMount(() => {
|
|
39
|
+
// The status carries the host's warnings; a host page that mounts the
|
|
40
|
+
// selection alone has not loaded it yet.
|
|
41
|
+
if (node.status === null && !node.statusLoading) void node.loadStatus();
|
|
35
42
|
if (!unavailable) void node.loadExposure();
|
|
36
43
|
});
|
|
37
44
|
|
|
@@ -49,19 +56,21 @@
|
|
|
49
56
|
const schema = $derived(node.schema);
|
|
50
57
|
|
|
51
58
|
// The value filters: the host's fixed filters, then one per filterable
|
|
52
|
-
// column with the distinct values the items carry
|
|
53
|
-
|
|
59
|
+
// column with the distinct values the items carry — for a list-valued
|
|
60
|
+
// column the individual values, not their combinations.
|
|
61
|
+
type ValueFilter = { field: string; label: string; multi: boolean; options: { value: string; label: string }[] };
|
|
54
62
|
const valueFilters = $derived.by<ValueFilter[]>(() => {
|
|
55
63
|
if (!schema) return [];
|
|
56
|
-
const out: ValueFilter[] = schema.filters.map((f) => ({ field: f.field, label: f.label, options: f.options }));
|
|
64
|
+
const out: ValueFilter[] = schema.filters.map((f) => ({ field: f.field, label: f.label, multi: !!f.multi, options: f.options }));
|
|
57
65
|
for (const col of schema.columns) {
|
|
58
66
|
if (!col.filter || out.some((f) => f.field === col.key)) continue;
|
|
59
|
-
const values =
|
|
60
|
-
|
|
61
|
-
out.push({ field: col.key, label: col.label, options: values.map((v) => ({ value: v, label: v })) });
|
|
67
|
+
const values = distinctValues(node.items.map((i) => i.fields[col.key]), !!col.multi);
|
|
68
|
+
out.push({ field: col.key, label: col.label, multi: !!col.multi, options: values.map((v) => ({ value: v, label: v })) });
|
|
62
69
|
}
|
|
63
70
|
return out;
|
|
64
71
|
});
|
|
72
|
+
const multiField = $derived(new Set(valueFilters.filter((f) => f.multi).map((f) => f.field)));
|
|
73
|
+
const multiColumn = $derived(new Set((schema?.columns ?? []).filter((c) => c.multi).map((c) => "field:" + c.key)));
|
|
65
74
|
|
|
66
75
|
// The item as the page shows it: with its pending change applied.
|
|
67
76
|
function current(item: Item): Item {
|
|
@@ -74,7 +83,7 @@
|
|
|
74
83
|
const match = makeTextMatcher(search);
|
|
75
84
|
return node.items.filter((item) => {
|
|
76
85
|
for (const [field, value] of Object.entries(filterValues)) {
|
|
77
|
-
if (value !== ALL && item.fields[field]
|
|
86
|
+
if (value !== ALL && !fieldMatches(item.fields[field], value, multiField.has(field))) return false;
|
|
78
87
|
}
|
|
79
88
|
if (!search.trim()) return true;
|
|
80
89
|
const it = current(item);
|
|
@@ -218,6 +227,15 @@
|
|
|
218
227
|
</button>
|
|
219
228
|
{:else if col.key === "device_type"}
|
|
220
229
|
<span class="text-[var(--ha-secondary-text-color)]">{item.verdict.device_type_name || "—"}</span>
|
|
230
|
+
{:else if multiColumn.has(col.key)}
|
|
231
|
+
{@const values = listValues(item.fields[col.key.slice("field:".length)])}
|
|
232
|
+
{#if values.length === 0}
|
|
233
|
+
<span class="text-[var(--ha-secondary-text-color)]">—</span>
|
|
234
|
+
{:else}
|
|
235
|
+
<span class="inline-flex flex-wrap gap-1">
|
|
236
|
+
{#each values as v, i (i)}<Badge variant="muted">{v}</Badge>{/each}
|
|
237
|
+
</span>
|
|
238
|
+
{/if}
|
|
221
239
|
{:else}
|
|
222
240
|
<span class="text-[var(--ha-secondary-text-color)]">{col.get?.(item) || "—"}</span>
|
|
223
241
|
{/if}
|
|
@@ -230,6 +248,7 @@
|
|
|
230
248
|
{:else if node.exposureError}
|
|
231
249
|
<ErrorState message={node.exposureError} onRetry={() => void node.loadExposure()} />
|
|
232
250
|
{:else if schema}
|
|
251
|
+
<HostWarnings warnings={node.status?.warnings ?? []} collapsed />
|
|
233
252
|
<div class="mb-3 flex flex-col gap-2">
|
|
234
253
|
<div class="flex flex-wrap items-center gap-2">
|
|
235
254
|
<Input placeholder={t("node.selection.search_placeholder")} bind:value={search} class="w-full sm:w-64" />
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<!-- The node's status: whether it runs, listens, is healthy and
|
|
2
2
|
advertised, its counts and the commissioning window. Ported from the
|
|
3
3
|
status card of openccu-loom assets/ui/src/routes/matter/Matter.svelte:47-100
|
|
4
|
-
at b40b289a, without the tabs (the shell or the host navigates).
|
|
4
|
+
at b40b289a, without the tabs (the shell or the host navigates). The
|
|
5
|
+
host's configuration warnings come first, in their own card. -->
|
|
5
6
|
<script lang="ts">
|
|
6
7
|
import { onMount } from "svelte";
|
|
7
8
|
import { useFabricUI } from "../../context";
|
|
@@ -9,6 +10,7 @@
|
|
|
9
10
|
import Card from "../../kit/Card.svelte";
|
|
10
11
|
import ErrorState from "../../kit/ErrorState.svelte";
|
|
11
12
|
import LoadingState from "../../kit/LoadingState.svelte";
|
|
13
|
+
import HostWarnings from "./HostWarnings.svelte";
|
|
12
14
|
|
|
13
15
|
const { t, node } = useFabricUI();
|
|
14
16
|
|
|
@@ -24,10 +26,12 @@
|
|
|
24
26
|
{:else if node.statusError && !s}
|
|
25
27
|
<ErrorState message={node.statusError} onRetry={() => void node.loadStatus()} />
|
|
26
28
|
{:else if s && !s.enabled}
|
|
29
|
+
<HostWarnings warnings={s.warnings ?? []} />
|
|
27
30
|
<Card class="p-4">
|
|
28
31
|
<p class="text-sm font-medium text-[var(--ha-secondary-text-color)]">{t("node.status.disabled")}</p>
|
|
29
32
|
</Card>
|
|
30
33
|
{:else if s}
|
|
34
|
+
<HostWarnings warnings={s.warnings ?? []} />
|
|
31
35
|
<Card class="p-4" data-testid="node-status">
|
|
32
36
|
<div class="flex flex-wrap items-center gap-4">
|
|
33
37
|
<span class="font-medium text-[var(--ha-primary-text-color)]">{t("node.status.enabled")}</span>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// List-valued item fields (Column.multi, Filter.multi): the provider
|
|
2
|
+
// carries several values as one comma-separated string.
|
|
3
|
+
|
|
4
|
+
/** The values of a comma-separated field, trimmed, without empty ones. */
|
|
5
|
+
export function listValues(field: string | undefined): string[] {
|
|
6
|
+
if (!field) return [];
|
|
7
|
+
return field
|
|
8
|
+
.split(",")
|
|
9
|
+
.map((v) => v.trim())
|
|
10
|
+
.filter((v) => v !== "");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Whether an item's field matches a filter value: by equality, or, for a
|
|
15
|
+
* list-valued field, by membership in its list. An empty value never
|
|
16
|
+
* matches a list.
|
|
17
|
+
*/
|
|
18
|
+
export function fieldMatches(field: string | undefined, value: string, multi: boolean): boolean {
|
|
19
|
+
if (!multi) return field === value;
|
|
20
|
+
const v = value.trim();
|
|
21
|
+
return v !== "" && listValues(field).includes(v);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The distinct filter values of a field across items, sorted for display. */
|
|
25
|
+
export function distinctValues(fields: (string | undefined)[], multi: boolean): string[] {
|
|
26
|
+
const values = new Set<string>();
|
|
27
|
+
for (const f of fields) {
|
|
28
|
+
if (multi) for (const v of listValues(f)) values.add(v);
|
|
29
|
+
else if (f) values.add(f);
|
|
30
|
+
}
|
|
31
|
+
return [...values].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base", numeric: true }));
|
|
32
|
+
}
|