@reddb-io/ui 0.3.0 → 0.3.2
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/build/_app/immutable/assets/0.BWZTMKZW.css +1 -0
- package/build/_app/immutable/assets/AnalyticsView.BLAJezw4.css +1 -0
- package/build/_app/immutable/assets/{Workspace.CvP5De7i.css → Workspace.DM2E1WiF.css} +1 -1
- package/build/_app/immutable/chunks/{C9rXzYLu.js → B-yl15ta.js} +1 -1
- package/build/_app/immutable/chunks/B5euRbKa.js +1 -0
- package/build/_app/immutable/chunks/BRIMmWzq.js +1 -0
- package/build/_app/immutable/chunks/BgOTHNR5.js +1 -0
- package/build/_app/immutable/chunks/Bgb6fHGb.js +18 -0
- package/build/_app/immutable/chunks/Btd6PFkC.js +1 -0
- package/build/_app/immutable/chunks/C-kqMbRi.js +1 -0
- package/build/_app/immutable/chunks/C3SGj6Qd.js +2 -0
- package/build/_app/immutable/chunks/{C278awGv.js → C7kf23SQ.js} +1 -1
- package/build/_app/immutable/chunks/CGFL56WJ.js +1 -0
- package/build/_app/immutable/chunks/CHvQYbpl.js +1 -0
- package/build/_app/immutable/chunks/CO7K4bD2.js +1164 -0
- package/build/_app/immutable/chunks/CP65IkJc.js +1 -0
- package/build/_app/immutable/chunks/CxnvJShw.js +404 -0
- package/build/_app/immutable/chunks/D02PB2u7.js +1 -0
- package/build/_app/immutable/chunks/DCJ0QyYH.js +1 -0
- package/build/_app/immutable/chunks/DN7v3Tar.js +2 -0
- package/build/_app/immutable/chunks/DRlh90Cv.js +1 -0
- package/build/_app/immutable/entry/app.BeX4Tmu5.js +2 -0
- package/build/_app/immutable/entry/start.D7y1MZex.js +1 -0
- package/build/_app/immutable/nodes/0.C0e5KQVb.js +2 -0
- package/build/_app/immutable/nodes/1.ceiVuZbB.js +1 -0
- package/build/_app/immutable/nodes/10.DeFqKRR2.js +96 -0
- package/build/_app/immutable/nodes/12.CzMvqoYS.js +3 -0
- package/build/_app/immutable/nodes/14.CAG5_cGP.js +1 -0
- package/build/_app/immutable/nodes/15.CRDBqbiH.js +60 -0
- package/build/_app/immutable/nodes/8.CAG5_cGP.js +1 -0
- package/build/_app/version.json +1 -1
- package/build/index.html +18 -15
- package/dist/embed/AnalyticsView-SKyGxeAx.js +23337 -0
- package/dist/embed/AnalyticsView-SKyGxeAx.js.map +1 -0
- package/dist/embed/RedUiEmbedRoot-BzVzGAFK.js +31960 -0
- package/dist/embed/RedUiEmbedRoot-BzVzGAFK.js.map +1 -0
- package/dist/embed/event-BvRbkLjD.js +46 -0
- package/dist/embed/event-BvRbkLjD.js.map +1 -0
- package/dist/embed/index-0hA141D2.js +85 -0
- package/dist/embed/index-0hA141D2.js.map +1 -0
- package/dist/embed/index-BVxMkfHe.js +8 -0
- package/dist/embed/index-BVxMkfHe.js.map +1 -0
- package/dist/embed/index-BY6fc1oQ.js +856 -0
- package/dist/embed/index-BY6fc1oQ.js.map +1 -0
- package/dist/embed/index-client-BHUUkI8H.js +3304 -0
- package/dist/embed/index-client-BHUUkI8H.js.map +1 -0
- package/dist/embed/index.js +1 -1
- package/dist/embed/{sigma.esm-D5yoExsH.js → sigma.esm-BawyGZ3s.js} +2 -2
- package/dist/embed/{sigma.esm-D5yoExsH.js.map → sigma.esm-BawyGZ3s.js.map} +1 -1
- package/dist/embed/window-ZR8_mM8o.js +1889 -0
- package/dist/embed/window-ZR8_mM8o.js.map +1 -0
- package/package.json +7 -3
- package/src/app.css +86 -20
- package/src/lib/AnalyticsView.svelte +209 -0
- package/src/lib/AskView.svelte +255 -0
- package/src/lib/ClusterView.svelte +415 -95
- package/src/lib/CommandPalette.svelte +2 -0
- package/src/lib/ConnectDropdown.svelte +60 -3
- package/src/lib/DevConsole.svelte +155 -0
- package/src/lib/ErrorBoundary.fixture.svelte +11 -0
- package/src/lib/ErrorBoundary.fixtures.svelte.ts +5 -0
- package/src/lib/ErrorBoundary.svelte +78 -0
- package/src/lib/ErrorBoundaryCrashChild.fixture.svelte +9 -0
- package/src/lib/GlobalProgressBar.svelte +9 -0
- package/src/lib/LoadingOverlay.svelte +109 -0
- package/src/lib/MetricChart.svelte +85 -0
- package/src/lib/QueryEditor.svelte +48 -7
- package/src/lib/ResultsPane.svelte +76 -8
- package/src/lib/SchemaTree.svelte +25 -1
- package/src/lib/SecurityView.svelte +176 -65
- package/src/lib/SettingsView.svelte +1111 -254
- package/src/lib/StatusBar.svelte +19 -1
- package/src/lib/Topbar.svelte +29 -1
- package/src/lib/Workspace.svelte +136 -5
- package/src/lib/activity.svelte.ts +44 -35
- package/src/lib/collection-preview.ts +14 -0
- package/src/lib/connections.svelte.ts +167 -4
- package/src/lib/embed/RedUiEmbedRoot.svelte +13 -8
- package/src/lib/embed/index.ts +34 -1
- package/src/lib/index.ts +10 -0
- package/src/lib/loading.svelte.ts +142 -0
- package/src/lib/permission-gate.ts +120 -0
- package/src/lib/policies.ts +136 -0
- package/src/lib/query-tabs.svelte.ts +132 -53
- package/src/lib/reddb/analytics.ts +208 -0
- package/src/lib/reddb/ask.ts +123 -0
- package/src/lib/reddb/client.ts +292 -73
- package/src/lib/reddb/cluster-status.ts +306 -0
- package/src/lib/reddb/desktop-error.ts +89 -0
- package/src/lib/reddb/dev-console.ts +186 -0
- package/src/lib/reddb/index.ts +5 -0
- package/src/lib/reddb/local-url-provider.ts +28 -1
- package/src/lib/reddb/redwire-ws-spike.ts +590 -0
- package/src/lib/reddb/tauri-encrypted-store.ts +4 -9
- package/src/lib/router.svelte.ts +12 -0
- package/src/lib/settings-authoring-client.ts +336 -0
- package/src/lib/settings-sections.ts +292 -196
- package/src/lib/tabs.svelte.ts +5 -0
- package/src/lib/workspace-session.ts +81 -0
- package/build/_app/immutable/assets/0.DQDLkSdR.css +0 -1
- package/build/_app/immutable/chunks/65XjEwaF.js +0 -367
- package/build/_app/immutable/chunks/BGcIe2ep.js +0 -2
- package/build/_app/immutable/chunks/BjFADNkA.js +0 -992
- package/build/_app/immutable/chunks/BrvvOVaJ.js +0 -1
- package/build/_app/immutable/chunks/D9CTkMjK.js +0 -1
- package/build/_app/immutable/chunks/oiO4RLqp.js +0 -1
- package/build/_app/immutable/chunks/pDJnDxXM.js +0 -1
- package/build/_app/immutable/entry/app.DdaMXHAv.js +0 -2
- package/build/_app/immutable/entry/start.-MLBtQXB.js +0 -1
- package/build/_app/immutable/nodes/0.Ysoz3Y3h.js +0 -1
- package/build/_app/immutable/nodes/1.C2jbGTOJ.js +0 -1
- package/build/_app/immutable/nodes/12.DQG5zpEx.js +0 -60
- package/build/_app/immutable/nodes/8.CrHI4F9p.js +0 -134
- package/dist/embed/RedUiEmbedRoot-BwFUA5X-.js +0 -28624
- package/dist/embed/RedUiEmbedRoot-BwFUA5X-.js.map +0 -1
- package/dist/embed/index-DGhgtnaK.js +0 -577
- package/dist/embed/index-DGhgtnaK.js.map +0 -1
- package/dist/embed/index-client-ItCEU64C.js +0 -3087
- package/dist/embed/index-client-ItCEU64C.js.map +0 -1
- /package/build/_app/immutable/assets/{12.Dblyk0Kr.css → 15.Dblyk0Kr.css} +0 -0
- /package/build/_app/immutable/nodes/{10.CAG5_cGP.js → 13.CAG5_cGP.js} +0 -0
|
@@ -1,29 +1,27 @@
|
|
|
1
|
-
// Data-driven settings registry +
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
// Kept framework-free (no Svelte imports) so the registry and the resolver are
|
|
12
|
-
// trivially unit-testable. Icons are mapped to components in `SettingsView`,
|
|
13
|
-
// not here — a section carries only an icon *name*. Mirrors the registry idiom
|
|
14
|
-
// in `renderers/registry`: a tiny pure surface over curated data.
|
|
1
|
+
// Data-driven settings registry + pure schema resolver for the Settings surface.
|
|
2
|
+
import type {
|
|
3
|
+
ConfigEntry,
|
|
4
|
+
ConfigMutationValue,
|
|
5
|
+
ConfigValueType,
|
|
6
|
+
SecretEntry,
|
|
7
|
+
SecretReference,
|
|
8
|
+
} from "./settings-authoring-client";
|
|
9
|
+
import type { PermissionCheck } from "./permission-gate";
|
|
15
10
|
|
|
16
|
-
/** The
|
|
17
|
-
export type ControlKind =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
/** The read-only control a config value is rendered through. */
|
|
12
|
+
export type ControlKind =
|
|
13
|
+
| "text"
|
|
14
|
+
| "number"
|
|
15
|
+
| "boolean"
|
|
16
|
+
| "enum"
|
|
17
|
+
| "list"
|
|
18
|
+
| "secret-reference";
|
|
21
19
|
|
|
22
20
|
/** A resolved control for one config key. */
|
|
23
21
|
export interface ControlDescriptor {
|
|
24
22
|
/** The config path this descriptor was resolved from. */
|
|
25
23
|
key: string;
|
|
26
|
-
/** Human label
|
|
24
|
+
/** Human label: a curated override or a humanized key. */
|
|
27
25
|
label: string;
|
|
28
26
|
/** Optional one-line explanation. */
|
|
29
27
|
description?: string;
|
|
@@ -31,10 +29,18 @@ export interface ControlDescriptor {
|
|
|
31
29
|
kind: ControlKind;
|
|
32
30
|
/** Allowed values when `kind === "enum"`. */
|
|
33
31
|
options?: readonly string[];
|
|
32
|
+
/** Server-reported config schema type, when SHOW CONFIG projects it. */
|
|
33
|
+
valueType?: ConfigValueType;
|
|
34
|
+
/** Server-reported config schema version, when SHOW CONFIG projects it. */
|
|
35
|
+
schemaVersion?: number;
|
|
36
|
+
/** True when this row must not render plaintext. */
|
|
37
|
+
masked?: boolean;
|
|
38
|
+
/** Vault path referenced by a config value, if this is a secret reference. */
|
|
39
|
+
secretReference?: SecretReference;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
|
-
export interface
|
|
37
|
-
/** Stable identifier used as the active-
|
|
42
|
+
export interface SettingsPane {
|
|
43
|
+
/** Stable identifier used as the active-pane key (component state only). */
|
|
38
44
|
id: string;
|
|
39
45
|
/** Nav label. */
|
|
40
46
|
label: string;
|
|
@@ -42,119 +48,120 @@ export interface SettingsSection {
|
|
|
42
48
|
blurb: string;
|
|
43
49
|
/** Icon name resolved to a lucide component in `SettingsView`. */
|
|
44
50
|
icon: string;
|
|
45
|
-
/**
|
|
46
|
-
|
|
51
|
+
/** Pane-level grant that must be allowed before this pane is rendered. */
|
|
52
|
+
readGrant: PermissionCheck;
|
|
53
|
+
/** Pane-level grant that enables authoring controls when present. */
|
|
54
|
+
writeGrant?: PermissionCheck;
|
|
55
|
+
/** Pane-level grant that enables break-glass secret reveal controls. */
|
|
56
|
+
revealGrant?: PermissionCheck;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
|
|
50
|
-
* The curated sections. Each `keys` entry is a flattened reddb config path
|
|
51
|
-
* (see `flattenConfig`) sourced from `/stats`, `/cluster/status`,
|
|
52
|
-
* `/capabilities`, or the session (`whoami`). Order is the render order.
|
|
53
|
-
*/
|
|
54
|
-
export const SECTIONS: SettingsSection[] = [
|
|
59
|
+
export const SETTINGS_PANES: SettingsPane[] = [
|
|
55
60
|
{
|
|
56
|
-
id: "
|
|
57
|
-
label: "
|
|
58
|
-
blurb: "
|
|
61
|
+
id: "config",
|
|
62
|
+
label: "Config",
|
|
63
|
+
blurb: "Live non-secret configuration from SHOW CONFIG.",
|
|
59
64
|
icon: "settings",
|
|
60
|
-
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
id: "storage",
|
|
71
|
-
label: "Storage",
|
|
72
|
-
blurb: "Live store footprint for the active connection.",
|
|
73
|
-
icon: "database",
|
|
74
|
-
keys: [
|
|
75
|
-
"store.collection_count",
|
|
76
|
-
"store.total_entities",
|
|
77
|
-
"store.cross_ref_count",
|
|
78
|
-
"store.total_memory_bytes",
|
|
79
|
-
],
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
id: "cluster",
|
|
83
|
-
label: "Cluster",
|
|
84
|
-
blurb: "Deployment and replication topology.",
|
|
85
|
-
icon: "network",
|
|
86
|
-
keys: [
|
|
87
|
-
"deployment.mode",
|
|
88
|
-
"deployment.file_path",
|
|
89
|
-
"replication.replica_count",
|
|
90
|
-
"replication.lag_ms",
|
|
91
|
-
],
|
|
65
|
+
readGrant: {
|
|
66
|
+
action: "config:read",
|
|
67
|
+
resource: { kind: "config", name: "*" },
|
|
68
|
+
},
|
|
69
|
+
writeGrant: {
|
|
70
|
+
action: "config:write",
|
|
71
|
+
resource: { kind: "config", name: "*" },
|
|
72
|
+
},
|
|
92
73
|
},
|
|
93
74
|
{
|
|
94
|
-
id: "
|
|
95
|
-
label: "
|
|
96
|
-
blurb: "
|
|
97
|
-
icon: "
|
|
98
|
-
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
keys: ["session.username", "session.role", "session.authenticated"],
|
|
75
|
+
id: "secrets",
|
|
76
|
+
label: "Secrets",
|
|
77
|
+
blurb: "Vault inventory from SHOW SECRETS with values masked.",
|
|
78
|
+
icon: "key",
|
|
79
|
+
readGrant: {
|
|
80
|
+
action: "vault:read_metadata",
|
|
81
|
+
resource: { kind: "vault", name: "*" },
|
|
82
|
+
},
|
|
83
|
+
writeGrant: {
|
|
84
|
+
action: "vault:write",
|
|
85
|
+
resource: { kind: "vault", name: "*" },
|
|
86
|
+
},
|
|
87
|
+
revealGrant: {
|
|
88
|
+
action: "vault:read",
|
|
89
|
+
resource: { kind: "vault", name: "*" },
|
|
90
|
+
},
|
|
111
91
|
},
|
|
112
92
|
];
|
|
113
93
|
|
|
114
94
|
/** Curated label overrides, keyed by config path. */
|
|
115
95
|
export const LABELS: Record<string, string> = {
|
|
116
|
-
"
|
|
117
|
-
"
|
|
118
|
-
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
96
|
+
"durability.mode": "Durability mode",
|
|
97
|
+
"concurrency.locking.enabled": "Locking",
|
|
98
|
+
"cache.blob.l1_bytes_max": "Blob L1 cache",
|
|
99
|
+
"cache.blob.l2_bytes_max": "Blob L2 cache",
|
|
100
|
+
"red.auth.enabled": "Authentication",
|
|
101
|
+
"red.auth.require_auth": "Require authentication",
|
|
102
|
+
"red.auth.session_ttl_secs": "Session TTL",
|
|
103
|
+
"red.backup.backend": "Backup backend",
|
|
104
|
+
"red.backup.enabled": "Backups",
|
|
105
|
+
"red.backup.interval_secs": "Backup interval",
|
|
106
|
+
"red.cdc.enabled": "Change data capture",
|
|
107
|
+
"red.config.secret.auto_decrypt": "Auto-decrypt config secrets",
|
|
108
|
+
"red.config.secret.auto_encrypt": "Auto-encrypt config secrets",
|
|
109
|
+
"red.server.max_body_size": "Max request body",
|
|
110
|
+
"red.server.max_scan_limit": "Max scan limit",
|
|
111
|
+
"red.server.read_timeout_ms": "Read timeout",
|
|
112
|
+
"red.server.write_timeout_ms": "Write timeout",
|
|
113
|
+
"red.storage.page_size": "Page size",
|
|
114
|
+
"red.storage.page_cache_capacity": "Page cache capacity",
|
|
115
|
+
"red.storage.verify_checksums": "Verify checksums",
|
|
116
|
+
"red.system.os": "Operating system",
|
|
117
|
+
"red.system.arch": "Architecture",
|
|
118
|
+
"red.system.hostname": "Hostname",
|
|
119
|
+
"red.system.cpu_cores": "CPU cores",
|
|
120
|
+
"red.system.total_memory_bytes": "System memory",
|
|
121
|
+
"red.vcs.default_branch": "Default branch",
|
|
122
|
+
"red.vcs.protected_branches": "Protected branches",
|
|
123
|
+
"runtime.result_cache.backend": "Result cache backend",
|
|
124
|
+
"runtime.result_cache.enabled": "Result cache",
|
|
125
|
+
"storage.bgwriter.delay_ms": "Background writer delay",
|
|
126
|
+
"storage.btree.lehman_yao": "Lehman-Yao B-tree",
|
|
127
|
+
"storage.wal.max_interval_ms": "WAL max interval",
|
|
128
|
+
"system.bootstrap.completed": "Bootstrap completed",
|
|
129
|
+
"system.bootstrap.preset": "Bootstrap preset",
|
|
136
130
|
};
|
|
137
131
|
|
|
138
132
|
/** Curated descriptions, keyed by config path. */
|
|
139
133
|
export const DESCRIPTIONS: Record<string, string> = {
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"
|
|
134
|
+
"durability.mode": "Write durability policy currently reported by reddb.",
|
|
135
|
+
"red.auth.enabled":
|
|
136
|
+
"Whether reddb authentication is enabled for this instance.",
|
|
137
|
+
"red.auth.require_auth": "Whether unauthenticated requests are rejected.",
|
|
138
|
+
"red.backup.backend": "The selected backup storage backend.",
|
|
139
|
+
"red.backup.enabled": "Whether scheduled backups are enabled.",
|
|
140
|
+
"red.cdc.enabled": "Whether the change stream subsystem is enabled.",
|
|
141
|
+
"red.server.max_body_size":
|
|
142
|
+
"Maximum accepted HTTP request body size, in bytes.",
|
|
143
|
+
"red.server.max_scan_limit": "Default cap for scan-heavy API paths.",
|
|
144
|
+
"red.storage.page_size": "On-disk page size used by the storage engine.",
|
|
145
|
+
"red.storage.verify_checksums":
|
|
146
|
+
"Whether page checksums are verified on read.",
|
|
147
|
+
"red.vcs.default_branch":
|
|
148
|
+
"Branch name used when VCS features need a default.",
|
|
149
|
+
"red.vcs.protected_branches":
|
|
150
|
+
"Branches protected from destructive operations.",
|
|
151
|
+
"runtime.result_cache.enabled": "Whether query result caching is enabled.",
|
|
152
|
+
"system.bootstrap.preset": "The bootstrap profile that initialized defaults.",
|
|
148
153
|
};
|
|
149
154
|
|
|
150
155
|
/** Curated enum options, keyed by config path. Presence ⇒ `kind: "enum"`. */
|
|
151
156
|
export const ENUM_OPTIONS: Record<string, readonly string[]> = {
|
|
152
|
-
"
|
|
157
|
+
"durability.mode": ["sync", "async", "none"],
|
|
158
|
+
"red.backup.backend": ["local", "s3", "gcs"],
|
|
159
|
+
"runtime.result_cache.backend": ["legacy", "blob_cache"],
|
|
153
160
|
};
|
|
154
161
|
|
|
155
162
|
/**
|
|
156
163
|
* Turn a config path into a human label when nobody curated one:
|
|
157
|
-
* `store.total_memory_bytes`
|
|
164
|
+
* `store.total_memory_bytes` becomes "Total memory bytes". Uses the last path
|
|
158
165
|
* segment, swaps `_`/`-` for spaces, and sentence-cases the result.
|
|
159
166
|
*/
|
|
160
167
|
export function humanizeKey(key: string): string {
|
|
@@ -164,104 +171,193 @@ export function humanizeKey(key: string): string {
|
|
|
164
171
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
165
172
|
}
|
|
166
173
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
174
|
+
function kindFromValueType(
|
|
175
|
+
valueType: ConfigValueType | undefined
|
|
176
|
+
): ControlKind | null {
|
|
177
|
+
switch (valueType) {
|
|
178
|
+
case "bool":
|
|
179
|
+
case "boolean":
|
|
180
|
+
return "boolean";
|
|
181
|
+
case "int":
|
|
182
|
+
case "integer":
|
|
183
|
+
case "float":
|
|
184
|
+
case "number":
|
|
185
|
+
case "count":
|
|
186
|
+
return "number";
|
|
187
|
+
case "array":
|
|
188
|
+
case "list":
|
|
189
|
+
return "list";
|
|
190
|
+
case "object":
|
|
191
|
+
case "string":
|
|
192
|
+
case "text":
|
|
193
|
+
case "url":
|
|
194
|
+
return "text";
|
|
195
|
+
default:
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function kindFromRuntimeValue(value: unknown): ControlKind {
|
|
201
|
+
if (typeof value === "boolean") return "boolean";
|
|
202
|
+
if (typeof value === "number") return "number";
|
|
203
|
+
if (Array.isArray(value)) return "list";
|
|
204
|
+
return "text";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function isSecretReference(value: unknown): value is SecretReference {
|
|
208
|
+
return (
|
|
209
|
+
!!value &&
|
|
210
|
+
typeof value === "object" &&
|
|
211
|
+
!Array.isArray(value) &&
|
|
212
|
+
(value as SecretReference).type === "secret_ref" &&
|
|
213
|
+
typeof (value as SecretReference).key === "string"
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function resolveConfigControl(entry: ConfigEntry): ControlDescriptor {
|
|
218
|
+
const options = ENUM_OPTIONS[entry.key];
|
|
219
|
+
const secretReference = isSecretReference(entry.value)
|
|
220
|
+
? entry.value
|
|
221
|
+
: undefined;
|
|
222
|
+
const kind: ControlKind = secretReference
|
|
223
|
+
? "secret-reference"
|
|
224
|
+
: options
|
|
225
|
+
? "enum"
|
|
226
|
+
: (kindFromValueType(entry.valueType) ??
|
|
227
|
+
kindFromRuntimeValue(entry.value));
|
|
190
228
|
const descriptor: ControlDescriptor = {
|
|
191
|
-
key,
|
|
192
|
-
label: LABELS[key] ?? humanizeKey(key),
|
|
229
|
+
key: entry.key,
|
|
230
|
+
label: LABELS[entry.key] ?? humanizeKey(entry.key),
|
|
193
231
|
kind,
|
|
194
232
|
};
|
|
195
|
-
const description = DESCRIPTIONS[key];
|
|
233
|
+
const description = DESCRIPTIONS[entry.key];
|
|
196
234
|
if (description) descriptor.description = description;
|
|
197
235
|
if (options) descriptor.options = options;
|
|
236
|
+
if (entry.valueType) descriptor.valueType = entry.valueType;
|
|
237
|
+
if (entry.schemaVersion !== undefined)
|
|
238
|
+
descriptor.schemaVersion = entry.schemaVersion;
|
|
239
|
+
if (secretReference) descriptor.secretReference = secretReference;
|
|
198
240
|
return descriptor;
|
|
199
241
|
}
|
|
200
242
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
key.startsWith("wal.") ||
|
|
209
|
-
key.startsWith("throughput.") ||
|
|
210
|
-
key.startsWith("replication.")
|
|
211
|
-
) {
|
|
212
|
-
return "cluster";
|
|
213
|
-
}
|
|
214
|
-
return "stats";
|
|
243
|
+
export function resolveSecretControl(entry: SecretEntry): ControlDescriptor {
|
|
244
|
+
return {
|
|
245
|
+
key: entry.key,
|
|
246
|
+
label: humanizeKey(entry.key),
|
|
247
|
+
kind: "text",
|
|
248
|
+
masked: true,
|
|
249
|
+
};
|
|
215
250
|
}
|
|
216
251
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
*/
|
|
223
|
-
export function flattenConfig(
|
|
224
|
-
input: Record<string, unknown>
|
|
225
|
-
): Record<string, unknown> {
|
|
226
|
-
const out: Record<string, unknown> = {};
|
|
227
|
-
const walk = (obj: Record<string, unknown>, prefix: string) => {
|
|
228
|
-
for (const [k, v] of Object.entries(obj)) {
|
|
229
|
-
const key = prefix ? `${prefix}.${k}` : k;
|
|
230
|
-
if (v === undefined || v === null) continue;
|
|
231
|
-
if (typeof v === "object" && !Array.isArray(v)) {
|
|
232
|
-
walk(v as Record<string, unknown>, key);
|
|
233
|
-
} else {
|
|
234
|
-
out[key] = v;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
walk(input, "");
|
|
239
|
-
return out;
|
|
252
|
+
export function resolvePane(
|
|
253
|
+
id: string | null,
|
|
254
|
+
panes: readonly SettingsPane[] = SETTINGS_PANES
|
|
255
|
+
): SettingsPane {
|
|
256
|
+
return panes.find((pane) => pane.id === id) ?? panes[0] ?? SETTINGS_PANES[0];
|
|
240
257
|
}
|
|
241
258
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
export function resolveSection(id: string | null): SettingsSection {
|
|
248
|
-
return SECTIONS.find((s) => s.id === id) ?? SECTIONS[0];
|
|
259
|
+
export function filterSettingsPanesByGrant(
|
|
260
|
+
panes: readonly SettingsPane[],
|
|
261
|
+
gate: { cachedCan(check: PermissionCheck): boolean }
|
|
262
|
+
): SettingsPane[] {
|
|
263
|
+
return panes.filter((pane) => gate.cachedCan(pane.readGrant));
|
|
249
264
|
}
|
|
250
265
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
* the settings search is unit-testable without rendering. The settings view
|
|
256
|
-
* keeps one query string per section id, so switching sections preserves each
|
|
257
|
-
* section's typed query.
|
|
258
|
-
*/
|
|
259
|
-
export function filterKeys(keys: readonly string[], query: string): string[] {
|
|
266
|
+
export function filterConfigEntries(
|
|
267
|
+
entries: readonly ConfigEntry[],
|
|
268
|
+
query: string
|
|
269
|
+
): ConfigEntry[] {
|
|
260
270
|
const q = query.trim().toLowerCase();
|
|
261
|
-
if (!q) return [...
|
|
262
|
-
return
|
|
263
|
-
(
|
|
264
|
-
key.toLowerCase().includes(q) ||
|
|
265
|
-
|
|
271
|
+
if (!q) return [...entries];
|
|
272
|
+
return entries.filter(
|
|
273
|
+
(entry) =>
|
|
274
|
+
entry.key.toLowerCase().includes(q) ||
|
|
275
|
+
resolveConfigControl(entry).label.toLowerCase().includes(q) ||
|
|
276
|
+
(isSecretReference(entry.value) &&
|
|
277
|
+
entry.value.key.toLowerCase().includes(q))
|
|
266
278
|
);
|
|
267
279
|
}
|
|
280
|
+
|
|
281
|
+
export function filterSecretEntries(
|
|
282
|
+
entries: readonly SecretEntry[],
|
|
283
|
+
query: string
|
|
284
|
+
): SecretEntry[] {
|
|
285
|
+
const q = query.trim().toLowerCase();
|
|
286
|
+
if (!q) return [...entries];
|
|
287
|
+
return entries.filter(
|
|
288
|
+
(entry) =>
|
|
289
|
+
entry.key.toLowerCase().includes(q) ||
|
|
290
|
+
resolveSecretControl(entry).label.toLowerCase().includes(q) ||
|
|
291
|
+
entry.status?.toLowerCase().includes(q)
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface ParsedConfigEditValue {
|
|
296
|
+
ok: boolean;
|
|
297
|
+
value?: ConfigMutationValue;
|
|
298
|
+
error?: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function initialConfigEditValue(entry: ConfigEntry): string {
|
|
302
|
+
if (
|
|
303
|
+
Array.isArray(entry.value) ||
|
|
304
|
+
(entry.value && typeof entry.value === "object")
|
|
305
|
+
) {
|
|
306
|
+
return JSON.stringify(entry.value, null, 2);
|
|
307
|
+
}
|
|
308
|
+
if (entry.value === null || entry.value === undefined) return "";
|
|
309
|
+
return String(entry.value);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function parseConfigEditValue(
|
|
313
|
+
control: ControlDescriptor,
|
|
314
|
+
raw: string
|
|
315
|
+
): ParsedConfigEditValue {
|
|
316
|
+
const value = raw.trim();
|
|
317
|
+
switch (control.kind) {
|
|
318
|
+
case "boolean":
|
|
319
|
+
if (value === "true") return { ok: true, value: true };
|
|
320
|
+
if (value === "false") return { ok: true, value: false };
|
|
321
|
+
return {
|
|
322
|
+
ok: false,
|
|
323
|
+
error: "Boolean config values must be true or false.",
|
|
324
|
+
};
|
|
325
|
+
case "number": {
|
|
326
|
+
if (!value)
|
|
327
|
+
return { ok: false, error: "Number config values cannot be empty." };
|
|
328
|
+
const parsed = Number(value);
|
|
329
|
+
if (!Number.isFinite(parsed)) {
|
|
330
|
+
return { ok: false, error: "Number config values must be finite." };
|
|
331
|
+
}
|
|
332
|
+
return { ok: true, value: parsed };
|
|
333
|
+
}
|
|
334
|
+
case "enum":
|
|
335
|
+
if (!control.options?.includes(raw)) {
|
|
336
|
+
return { ok: false, error: "Choose one of the allowed config values." };
|
|
337
|
+
}
|
|
338
|
+
return { ok: true, value: raw };
|
|
339
|
+
case "list": {
|
|
340
|
+
try {
|
|
341
|
+
const parsed = JSON.parse(raw);
|
|
342
|
+
if (!Array.isArray(parsed)) {
|
|
343
|
+
return {
|
|
344
|
+
ok: false,
|
|
345
|
+
error: "List config values must be a JSON array.",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return { ok: true, value: parsed };
|
|
349
|
+
} catch {
|
|
350
|
+
return { ok: false, error: "List config values must be valid JSON." };
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
case "secret-reference":
|
|
354
|
+
return {
|
|
355
|
+
ok: false,
|
|
356
|
+
error:
|
|
357
|
+
"Secret references can be unset, but not edited as plaintext config.",
|
|
358
|
+
};
|
|
359
|
+
case "text":
|
|
360
|
+
default:
|
|
361
|
+
return { ok: true, value: raw };
|
|
362
|
+
}
|
|
363
|
+
}
|
package/src/lib/tabs.svelte.ts
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Workspace session persistence — snapshot the active view, open tabs, and
|
|
2
|
+
// router location to localStorage so the app can restore on next launch.
|
|
3
|
+
//
|
|
4
|
+
// The class is injectable (pass a `storage` in tests) and pure — no Svelte
|
|
5
|
+
// runes, no browser globals at import time — so the node vitest suite can
|
|
6
|
+
// exercise it directly with a MemStorage.
|
|
7
|
+
|
|
8
|
+
import type { Tab } from './tabs.svelte'
|
|
9
|
+
import type { View } from './router.svelte'
|
|
10
|
+
|
|
11
|
+
export interface WorkspaceSnapshot {
|
|
12
|
+
view: View
|
|
13
|
+
collection: string | null
|
|
14
|
+
subpage: string | null
|
|
15
|
+
tabs: Tab[]
|
|
16
|
+
activeTabId: string | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface KeyValueStorage {
|
|
20
|
+
getItem(key: string): string | null
|
|
21
|
+
setItem(key: string, value: string): void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const STORAGE_KEY = 'red-ui-workspace-session'
|
|
25
|
+
|
|
26
|
+
export class WorkspaceSession {
|
|
27
|
+
private _timer: ReturnType<typeof setTimeout> | null = null
|
|
28
|
+
readonly debounceMs: number
|
|
29
|
+
private readonly _storage: KeyValueStorage | null
|
|
30
|
+
|
|
31
|
+
constructor(debounceMs = 1000, storage: KeyValueStorage | null = null) {
|
|
32
|
+
this.debounceMs = debounceMs
|
|
33
|
+
this._storage = storage
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private _store(): KeyValueStorage | null {
|
|
37
|
+
if (this._storage) return this._storage
|
|
38
|
+
return typeof localStorage !== 'undefined' ? localStorage : null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private _write(snapshot: WorkspaceSnapshot): void {
|
|
42
|
+
try {
|
|
43
|
+
this._store()?.setItem(STORAGE_KEY, JSON.stringify(snapshot))
|
|
44
|
+
} catch {
|
|
45
|
+
// SSR or storage quota — silently skip
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Debounced save — resets the timer on every call. */
|
|
50
|
+
schedule(snapshot: WorkspaceSnapshot): void {
|
|
51
|
+
if (this._timer !== null) clearTimeout(this._timer)
|
|
52
|
+
this._timer = setTimeout(() => {
|
|
53
|
+
this._timer = null
|
|
54
|
+
this._write(snapshot)
|
|
55
|
+
}, this.debounceMs)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Cancel any pending debounce and save immediately. */
|
|
59
|
+
flush(snapshot: WorkspaceSnapshot): void {
|
|
60
|
+
if (this._timer !== null) {
|
|
61
|
+
clearTimeout(this._timer)
|
|
62
|
+
this._timer = null
|
|
63
|
+
}
|
|
64
|
+
this._write(snapshot)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Read the last persisted snapshot, or null if none or corrupted. */
|
|
68
|
+
restore(): WorkspaceSnapshot | null {
|
|
69
|
+
try {
|
|
70
|
+
const raw = this._store()?.getItem(STORAGE_KEY)
|
|
71
|
+
if (!raw) return null
|
|
72
|
+
const parsed = JSON.parse(raw) as WorkspaceSnapshot
|
|
73
|
+
if (typeof parsed.view !== 'string') return null
|
|
74
|
+
return parsed
|
|
75
|
+
} catch {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export const workspaceSession = new WorkspaceSession()
|