@happyvertical/smrt-features 0.51.18 → 0.51.20
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/AGENTS.md +17 -0
- package/README.md +152 -0
- package/dist/__smrt-register__.d.ts +2 -0
- package/dist/__smrt-register__.d.ts.map +1 -0
- package/dist/feature-definition.d.ts +18 -0
- package/dist/feature-definition.d.ts.map +1 -0
- package/dist/feature-definitions.d.ts +13 -0
- package/dist/feature-definitions.d.ts.map +1 -0
- package/dist/feature-override-service.d.ts +41 -0
- package/dist/feature-override-service.d.ts.map +1 -0
- package/dist/feature-override.d.ts +13 -0
- package/dist/feature-override.d.ts.map +1 -0
- package/dist/feature-overrides.d.ts +16 -0
- package/dist/feature-overrides.d.ts.map +1 -0
- package/dist/feature-resolver.d.ts +18 -0
- package/dist/feature-resolver.d.ts.map +1 -0
- package/dist/feature-settings.d.ts +209 -0
- package/dist/feature-settings.d.ts.map +1 -0
- package/dist/feature-sync.d.ts +17 -0
- package/dist/feature-sync.d.ts.map +1 -0
- package/dist/index.d.ts +11 -263
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +155 -2
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/smrt-knowledge.json +14 -6
- package/dist/svelte/components/FeatureSettingsPanel.svelte +338 -0
- package/dist/svelte/components/FeatureSettingsPanel.svelte.d.ts +52 -0
- package/dist/svelte/components/FeatureSettingsPanel.svelte.d.ts.map +1 -0
- package/dist/svelte/index.d.ts +17 -0
- package/dist/svelte/index.d.ts.map +1 -0
- package/dist/svelte/index.js +14 -0
- package/dist/svelte/types.d.ts +76 -0
- package/dist/svelte/types.d.ts.map +1 -0
- package/dist/svelte/types.js +37 -0
- package/dist/types.d.ts +107 -131
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +28 -0
- package/dist/utils.d.ts.map +1 -0
- package/package.json +26 -7
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* FeatureSettingsPanel — the shared feature-flag management surface.
|
|
4
|
+
*
|
|
5
|
+
* Purely presentational: it takes rows and callbacks. It does no fetching, and
|
|
6
|
+
* it makes no authorization decision — the host decides who may see the panel
|
|
7
|
+
* and who may write, and enforces that on the server (see
|
|
8
|
+
* `FeatureSettingsService` and `FeatureOverrideService`). Rendering the global
|
|
9
|
+
* column is a host decision too: apps with no platform-admin tier omit it.
|
|
10
|
+
*
|
|
11
|
+
* Each row renders as its own `<form>`, so a SvelteKit host can point
|
|
12
|
+
* `formAction` at a form action and get a working screen with no client JS;
|
|
13
|
+
* supplying `onSave` instead (or as well) intercepts the submit and hands the
|
|
14
|
+
* change to the host.
|
|
15
|
+
*/
|
|
16
|
+
import { Badge, Button, Card } from '@happyvertical/smrt-ui/ui';
|
|
17
|
+
import type { FeatureSettingsChange, FeatureSettingsView } from '../types.js';
|
|
18
|
+
import {
|
|
19
|
+
defaultOptionLabel,
|
|
20
|
+
effectiveStateLabel,
|
|
21
|
+
inheritedOptionLabel,
|
|
22
|
+
toFeatureSettingsEffect,
|
|
23
|
+
} from '../types.js';
|
|
24
|
+
|
|
25
|
+
export interface Props {
|
|
26
|
+
/** The features to manage, in the order they should be rendered. */
|
|
27
|
+
features?: FeatureSettingsView[];
|
|
28
|
+
/** Column heading for the tenant-scope control. */
|
|
29
|
+
tenantLabel?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Render the global-scope column. Off by default: an app with no
|
|
32
|
+
* platform-admin tier has nothing to put there.
|
|
33
|
+
*/
|
|
34
|
+
showGlobal?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Let the operator change the global column. Ignored unless `showGlobal`.
|
|
37
|
+
* When false the column is rendered read-only, which is how a tenant admin
|
|
38
|
+
* sees the platform default that their own override is layered on.
|
|
39
|
+
*/
|
|
40
|
+
globalEditable?: boolean;
|
|
41
|
+
/** Column heading for the global-scope control. */
|
|
42
|
+
globalLabel?: string;
|
|
43
|
+
/** Disable every control while a save is in flight. */
|
|
44
|
+
busy?: boolean;
|
|
45
|
+
/** Success text to show above the list. */
|
|
46
|
+
message?: string | null;
|
|
47
|
+
/** Error text to show above the list. */
|
|
48
|
+
error?: string | null;
|
|
49
|
+
/** Text for the empty state. */
|
|
50
|
+
emptyMessage?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Identity of what is being edited (typically the tenant id). Unsaved edits
|
|
53
|
+
* are discarded whenever this changes — or, when it is omitted, whenever
|
|
54
|
+
* `features` is replaced with a new array, which is what a reloaded server
|
|
55
|
+
* load produces. Without that, a row edited for one tenant would carry its
|
|
56
|
+
* unsaved choice into the next tenant's rows under the same feature key.
|
|
57
|
+
*/
|
|
58
|
+
contextKey?: unknown;
|
|
59
|
+
/** Label for each row's submit control. */
|
|
60
|
+
saveLabel?: string;
|
|
61
|
+
/**
|
|
62
|
+
* SvelteKit form action for each row's `<form>` (e.g. `?/saveFeature`).
|
|
63
|
+
* Omit when driving the panel entirely through {@link Props.onSave}.
|
|
64
|
+
*/
|
|
65
|
+
formAction?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Called with the requested change instead of submitting the form. The host
|
|
68
|
+
* performs its own permission check and the write.
|
|
69
|
+
*/
|
|
70
|
+
onSave?: (change: FeatureSettingsChange) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const {
|
|
74
|
+
features = [],
|
|
75
|
+
tenantLabel = 'Tenant',
|
|
76
|
+
showGlobal = false,
|
|
77
|
+
globalEditable = false,
|
|
78
|
+
globalLabel = 'Global',
|
|
79
|
+
busy = false,
|
|
80
|
+
contextKey,
|
|
81
|
+
message = null,
|
|
82
|
+
error = null,
|
|
83
|
+
emptyMessage = 'No features are registered yet. Definitions appear here once they are synced from code.',
|
|
84
|
+
saveLabel = 'Save',
|
|
85
|
+
formAction,
|
|
86
|
+
onSave,
|
|
87
|
+
}: Props = $props();
|
|
88
|
+
|
|
89
|
+
const globalIsEditable = $derived(showGlobal && globalEditable);
|
|
90
|
+
const canSubmit = $derived(!busy && (onSave != null || formAction != null));
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Remount the rows when the editing context changes, discarding unsaved edits.
|
|
94
|
+
* The selects are uncontrolled — their `selected` attributes seed them from
|
|
95
|
+
* `features` and the browser owns the value afterwards — so without this an
|
|
96
|
+
* operator's unsaved choice would survive a reload or a tenant switch and then
|
|
97
|
+
* be submitted against the new rows.
|
|
98
|
+
*/
|
|
99
|
+
const resetKey = $derived(contextKey ?? features);
|
|
100
|
+
|
|
101
|
+
function submit(event: SubmitEvent, feature: FeatureSettingsView): void {
|
|
102
|
+
if (!onSave) return;
|
|
103
|
+
event.preventDefault();
|
|
104
|
+
const fields = new FormData(event.currentTarget as HTMLFormElement);
|
|
105
|
+
onSave({
|
|
106
|
+
featureKey: feature.featureKey,
|
|
107
|
+
tenantEffect: toFeatureSettingsEffect(fields.get('tenantEffect')),
|
|
108
|
+
...(globalIsEditable
|
|
109
|
+
? { globalEffect: toFeatureSettingsEffect(fields.get('globalEffect')) }
|
|
110
|
+
: {}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Read-only rendering of an override level that this operator cannot change. */
|
|
115
|
+
function readOnlyEffectLabel(feature: FeatureSettingsView): string {
|
|
116
|
+
if (feature.globalEffect === 'enable') return 'Enabled';
|
|
117
|
+
if (feature.globalEffect === 'disable') return 'Disabled';
|
|
118
|
+
return defaultOptionLabel(feature.defaultEnabled);
|
|
119
|
+
}
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<section class="feature-settings" data-testid="feature-settings-panel">
|
|
123
|
+
{#if message}
|
|
124
|
+
<p class="fs-flash" role="status">{message}</p>
|
|
125
|
+
{/if}
|
|
126
|
+
{#if error}
|
|
127
|
+
<p class="fs-flash fs-flash-error" role="alert">{error}</p>
|
|
128
|
+
{/if}
|
|
129
|
+
|
|
130
|
+
{#key resetKey}
|
|
131
|
+
{#each features as feature (feature.featureKey)}
|
|
132
|
+
<Card variant="outlined">
|
|
133
|
+
<div class="fs-row">
|
|
134
|
+
<div class="fs-copy">
|
|
135
|
+
<div class="fs-title-row">
|
|
136
|
+
<h3 class="fs-title">{feature.label || feature.featureKey}</h3>
|
|
137
|
+
<Badge
|
|
138
|
+
variant={feature.effectiveEnabled ? 'success' : 'default'}
|
|
139
|
+
size="sm"
|
|
140
|
+
>
|
|
141
|
+
{effectiveStateLabel(feature.effectiveEnabled)}
|
|
142
|
+
</Badge>
|
|
143
|
+
</div>
|
|
144
|
+
<p class="fs-description">{feature.description || feature.featureKey}</p>
|
|
145
|
+
<code class="fs-key">{feature.featureKey}</code>
|
|
146
|
+
{#if feature.packageName}
|
|
147
|
+
<span class="fs-package">{feature.packageName}</span>
|
|
148
|
+
{/if}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<form
|
|
152
|
+
class="fs-controls"
|
|
153
|
+
method="POST"
|
|
154
|
+
action={formAction}
|
|
155
|
+
onsubmit={(event) => submit(event, feature)}
|
|
156
|
+
>
|
|
157
|
+
<input type="hidden" name="featureKey" value={feature.featureKey} />
|
|
158
|
+
|
|
159
|
+
{#if showGlobal}
|
|
160
|
+
{#if globalIsEditable}
|
|
161
|
+
<label class="fs-field">
|
|
162
|
+
<span class="fs-field-label">{globalLabel}</span>
|
|
163
|
+
<select name="globalEffect" disabled={busy}>
|
|
164
|
+
<option
|
|
165
|
+
value="inherit"
|
|
166
|
+
selected={feature.globalEffect !== 'enable' &&
|
|
167
|
+
feature.globalEffect !== 'disable'}
|
|
168
|
+
>
|
|
169
|
+
{defaultOptionLabel(feature.defaultEnabled)}
|
|
170
|
+
</option>
|
|
171
|
+
<option
|
|
172
|
+
value="enable"
|
|
173
|
+
selected={feature.globalEffect === 'enable'}
|
|
174
|
+
>
|
|
175
|
+
Enable
|
|
176
|
+
</option>
|
|
177
|
+
<option
|
|
178
|
+
value="disable"
|
|
179
|
+
selected={feature.globalEffect === 'disable'}
|
|
180
|
+
>
|
|
181
|
+
Disable
|
|
182
|
+
</option>
|
|
183
|
+
</select>
|
|
184
|
+
</label>
|
|
185
|
+
{:else}
|
|
186
|
+
<div class="fs-field">
|
|
187
|
+
<span class="fs-field-label">{globalLabel}</span>
|
|
188
|
+
<p class="fs-readonly">{readOnlyEffectLabel(feature)}</p>
|
|
189
|
+
</div>
|
|
190
|
+
{/if}
|
|
191
|
+
{/if}
|
|
192
|
+
|
|
193
|
+
<label class="fs-field">
|
|
194
|
+
<span class="fs-field-label">{tenantLabel}</span>
|
|
195
|
+
<select name="tenantEffect" disabled={busy}>
|
|
196
|
+
<option
|
|
197
|
+
value="inherit"
|
|
198
|
+
selected={feature.tenantEffect !== 'enable' &&
|
|
199
|
+
feature.tenantEffect !== 'disable'}
|
|
200
|
+
>
|
|
201
|
+
<!-- The tenant scope inherits from the levels above it, so
|
|
202
|
+
this never names the bare code default. -->
|
|
203
|
+
{inheritedOptionLabel(feature)}
|
|
204
|
+
</option>
|
|
205
|
+
<option value="enable" selected={feature.tenantEffect === 'enable'}>
|
|
206
|
+
Enable
|
|
207
|
+
</option>
|
|
208
|
+
<option
|
|
209
|
+
value="disable"
|
|
210
|
+
selected={feature.tenantEffect === 'disable'}
|
|
211
|
+
>
|
|
212
|
+
Disable
|
|
213
|
+
</option>
|
|
214
|
+
</select>
|
|
215
|
+
</label>
|
|
216
|
+
|
|
217
|
+
<Button type="submit" variant="primary" size="sm" disabled={!canSubmit}>
|
|
218
|
+
{saveLabel}
|
|
219
|
+
</Button>
|
|
220
|
+
</form>
|
|
221
|
+
</div>
|
|
222
|
+
</Card>
|
|
223
|
+
{:else}
|
|
224
|
+
<p class="fs-empty">{emptyMessage}</p>
|
|
225
|
+
{/each}
|
|
226
|
+
{/key}
|
|
227
|
+
</section>
|
|
228
|
+
|
|
229
|
+
<style>
|
|
230
|
+
.feature-settings {
|
|
231
|
+
display: flex;
|
|
232
|
+
flex-direction: column;
|
|
233
|
+
gap: 1rem;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.fs-flash {
|
|
237
|
+
margin: 0;
|
|
238
|
+
padding: 0.625rem 0.75rem;
|
|
239
|
+
border-radius: 0.375rem;
|
|
240
|
+
background: var(--color-surface-muted, #f3f4f6);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.fs-flash-error {
|
|
244
|
+
color: var(--color-error-text, #991b1b);
|
|
245
|
+
background: var(--color-error-surface, #fef2f2);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.fs-empty {
|
|
249
|
+
margin: 0;
|
|
250
|
+
color: var(--color-text-muted, #6b7280);
|
|
251
|
+
font-style: italic;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.fs-row {
|
|
255
|
+
display: grid;
|
|
256
|
+
grid-template-columns: minmax(0, 1fr) minmax(16rem, 22rem);
|
|
257
|
+
gap: 1rem;
|
|
258
|
+
align-items: start;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.fs-copy {
|
|
262
|
+
min-width: 0;
|
|
263
|
+
display: flex;
|
|
264
|
+
flex-direction: column;
|
|
265
|
+
gap: 0.25rem;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.fs-title-row {
|
|
269
|
+
display: flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
justify-content: space-between;
|
|
272
|
+
gap: 0.5rem;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.fs-title {
|
|
276
|
+
margin: 0;
|
|
277
|
+
font-size: 1rem;
|
|
278
|
+
line-height: 1.25;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.fs-description {
|
|
282
|
+
margin: 0;
|
|
283
|
+
color: var(--color-text-muted, #6b7280);
|
|
284
|
+
line-height: 1.45;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.fs-key,
|
|
288
|
+
.fs-package {
|
|
289
|
+
font-size: 0.8125rem;
|
|
290
|
+
color: var(--color-text-muted, #6b7280);
|
|
291
|
+
overflow-wrap: anywhere;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.fs-controls {
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: flex-end;
|
|
297
|
+
gap: 0.5rem;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.fs-field {
|
|
301
|
+
min-width: 0;
|
|
302
|
+
flex: 1 1 0;
|
|
303
|
+
display: flex;
|
|
304
|
+
flex-direction: column;
|
|
305
|
+
gap: 0.25rem;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.fs-field-label {
|
|
309
|
+
font-size: 0.8125rem;
|
|
310
|
+
color: var(--color-text-muted, #6b7280);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.fs-field select {
|
|
314
|
+
width: 100%;
|
|
315
|
+
padding: 0.375rem 0.5rem;
|
|
316
|
+
border: 1px solid var(--color-border, #d1d5db);
|
|
317
|
+
border-radius: 0.375rem;
|
|
318
|
+
background: var(--color-surface, #fff);
|
|
319
|
+
color: inherit;
|
|
320
|
+
font: inherit;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.fs-readonly {
|
|
324
|
+
margin: 0;
|
|
325
|
+
line-height: 1.45;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
@media (max-width: 860px) {
|
|
329
|
+
.fs-row {
|
|
330
|
+
grid-template-columns: 1fr;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.fs-controls {
|
|
334
|
+
flex-direction: column;
|
|
335
|
+
align-items: stretch;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
</style>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { FeatureSettingsChange, FeatureSettingsView } from '../types.js';
|
|
2
|
+
export interface Props {
|
|
3
|
+
/** The features to manage, in the order they should be rendered. */
|
|
4
|
+
features?: FeatureSettingsView[];
|
|
5
|
+
/** Column heading for the tenant-scope control. */
|
|
6
|
+
tenantLabel?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Render the global-scope column. Off by default: an app with no
|
|
9
|
+
* platform-admin tier has nothing to put there.
|
|
10
|
+
*/
|
|
11
|
+
showGlobal?: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Let the operator change the global column. Ignored unless `showGlobal`.
|
|
14
|
+
* When false the column is rendered read-only, which is how a tenant admin
|
|
15
|
+
* sees the platform default that their own override is layered on.
|
|
16
|
+
*/
|
|
17
|
+
globalEditable?: boolean;
|
|
18
|
+
/** Column heading for the global-scope control. */
|
|
19
|
+
globalLabel?: string;
|
|
20
|
+
/** Disable every control while a save is in flight. */
|
|
21
|
+
busy?: boolean;
|
|
22
|
+
/** Success text to show above the list. */
|
|
23
|
+
message?: string | null;
|
|
24
|
+
/** Error text to show above the list. */
|
|
25
|
+
error?: string | null;
|
|
26
|
+
/** Text for the empty state. */
|
|
27
|
+
emptyMessage?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Identity of what is being edited (typically the tenant id). Unsaved edits
|
|
30
|
+
* are discarded whenever this changes — or, when it is omitted, whenever
|
|
31
|
+
* `features` is replaced with a new array, which is what a reloaded server
|
|
32
|
+
* load produces. Without that, a row edited for one tenant would carry its
|
|
33
|
+
* unsaved choice into the next tenant's rows under the same feature key.
|
|
34
|
+
*/
|
|
35
|
+
contextKey?: unknown;
|
|
36
|
+
/** Label for each row's submit control. */
|
|
37
|
+
saveLabel?: string;
|
|
38
|
+
/**
|
|
39
|
+
* SvelteKit form action for each row's `<form>` (e.g. `?/saveFeature`).
|
|
40
|
+
* Omit when driving the panel entirely through {@link Props.onSave}.
|
|
41
|
+
*/
|
|
42
|
+
formAction?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Called with the requested change instead of submitting the form. The host
|
|
45
|
+
* performs its own permission check and the write.
|
|
46
|
+
*/
|
|
47
|
+
onSave?: (change: FeatureSettingsChange) => void;
|
|
48
|
+
}
|
|
49
|
+
declare const FeatureSettingsPanel: import("svelte").Component<Props, {}, "">;
|
|
50
|
+
type FeatureSettingsPanel = ReturnType<typeof FeatureSettingsPanel>;
|
|
51
|
+
export default FeatureSettingsPanel;
|
|
52
|
+
//# sourceMappingURL=FeatureSettingsPanel.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeatureSettingsPanel.svelte.d.ts","sourceRoot":"","sources":["../../../src/svelte/components/FeatureSettingsPanel.svelte.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAS9E,MAAM,WAAW,KAAK;IACpB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACjC,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,mDAAmD;IACnD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uDAAuD;IACvD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,yCAAyC;IACzC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,gCAAgC;IAChC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,CAAC;CAClD;AAgJD,QAAA,MAAM,oBAAoB,2CAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/smrt-features/svelte
|
|
3
|
+
*
|
|
4
|
+
* Optional Svelte 5 UI for feature-flag management. Presentational only: the
|
|
5
|
+
* panel takes rows and callbacks, does no fetching, and makes no authorization
|
|
6
|
+
* decision. Hosts render it from a server load that used
|
|
7
|
+
* `FeatureSettingsService` and wire saves to their own permission-checked
|
|
8
|
+
* route. The root `@happyvertical/smrt-features` export stays Svelte-free.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
import type { ComponentProps } from 'svelte';
|
|
13
|
+
import FeatureSettingsPanel from './components/FeatureSettingsPanel.svelte';
|
|
14
|
+
export { FeatureSettingsPanel };
|
|
15
|
+
export type FeatureSettingsPanelProps = ComponentProps<typeof FeatureSettingsPanel>;
|
|
16
|
+
export { defaultOptionLabel, effectiveStateLabel, type FeatureSettingsChange, type FeatureSettingsEffect, type FeatureSettingsView, inheritedOptionLabel, toFeatureSettingsEffect, } from './types.js';
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/svelte/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,oBAAoB,MAAM,0CAA0C,CAAC;AAE5E,OAAO,EAAE,oBAAoB,EAAE,CAAC;AAChC,MAAM,MAAM,yBAAyB,GAAG,cAAc,CACpD,OAAO,oBAAoB,CAC5B,CAAC;AAEF,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @happyvertical/smrt-features/svelte
|
|
3
|
+
*
|
|
4
|
+
* Optional Svelte 5 UI for feature-flag management. Presentational only: the
|
|
5
|
+
* panel takes rows and callbacks, does no fetching, and makes no authorization
|
|
6
|
+
* decision. Hosts render it from a server load that used
|
|
7
|
+
* `FeatureSettingsService` and wire saves to their own permission-checked
|
|
8
|
+
* route. The root `@happyvertical/smrt-features` export stays Svelte-free.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
import FeatureSettingsPanel from './components/FeatureSettingsPanel.svelte';
|
|
13
|
+
export { FeatureSettingsPanel };
|
|
14
|
+
export { defaultOptionLabel, effectiveStateLabel, inheritedOptionLabel, toFeatureSettingsEffect, } from './types.js';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentational types for the feature-flag management UI.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately free of the `@smrt()` model classes and of `@happyvertical/smrt-core`
|
|
5
|
+
* so the Svelte bundle stays a thin presentation layer. The effect strings match
|
|
6
|
+
* `FeatureOverrideEffect`'s values, so a `FeatureSettingsRow` produced by
|
|
7
|
+
* `FeatureSettingsService.listFeatureSettings()` is structurally assignable to
|
|
8
|
+
* {@link FeatureSettingsView} with no mapping step.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/** The three states an override control offers, matching `FeatureOverrideEffect`. */
|
|
13
|
+
export type FeatureSettingsEffect = 'inherit' | 'enable' | 'disable';
|
|
14
|
+
/** A render-ready view of one feature's settings. */
|
|
15
|
+
export interface FeatureSettingsView {
|
|
16
|
+
/**
|
|
17
|
+
* The persisted identifier. Shown verbatim so an operator can correlate the
|
|
18
|
+
* row with code and with support requests — renaming a key orphans its
|
|
19
|
+
* overrides, so it is stable enough to quote.
|
|
20
|
+
*/
|
|
21
|
+
featureKey: string;
|
|
22
|
+
/** Human label; falls back to the key when empty. */
|
|
23
|
+
label?: string;
|
|
24
|
+
/** Human description; falls back to the key when empty. */
|
|
25
|
+
description?: string;
|
|
26
|
+
/** Owning package, shown as provenance when present. */
|
|
27
|
+
packageName?: string;
|
|
28
|
+
/** The code-owned default, used to label the "Default" option. */
|
|
29
|
+
defaultEnabled: boolean;
|
|
30
|
+
/** What the resolver reports for the current context. */
|
|
31
|
+
effectiveEnabled: boolean;
|
|
32
|
+
/** Effect of the global override row, or `null`/absent when there is none. */
|
|
33
|
+
globalEffect?: FeatureSettingsEffect | null;
|
|
34
|
+
/** Effect of the tenant override row, or `null`/absent when there is none. */
|
|
35
|
+
tenantEffect?: FeatureSettingsEffect | null;
|
|
36
|
+
/**
|
|
37
|
+
* What this feature resolves to for this tenant with **no override of its
|
|
38
|
+
* own** — the state returning the tenant scope to Default actually produces.
|
|
39
|
+
*
|
|
40
|
+
* Only the server can know this: with a tenant hierarchy configured, an
|
|
41
|
+
* ancestor tenant's override sits between the global scope and this tenant,
|
|
42
|
+
* so it is not derivable from `defaultEnabled` and `globalEffect`. Leave it
|
|
43
|
+
* `null`/absent when it is not known and the panel says "Default (inherited)"
|
|
44
|
+
* rather than naming a state it cannot vouch for.
|
|
45
|
+
*/
|
|
46
|
+
inheritedEnabled?: boolean | null;
|
|
47
|
+
}
|
|
48
|
+
/** One override change requested from the panel. */
|
|
49
|
+
export interface FeatureSettingsChange {
|
|
50
|
+
/** The feature the operator was editing. */
|
|
51
|
+
featureKey: string;
|
|
52
|
+
/** Requested tenant-scope effect. */
|
|
53
|
+
tenantEffect: FeatureSettingsEffect;
|
|
54
|
+
/**
|
|
55
|
+
* Requested global-scope effect — present only when the panel rendered an
|
|
56
|
+
* editable global column.
|
|
57
|
+
*/
|
|
58
|
+
globalEffect?: FeatureSettingsEffect;
|
|
59
|
+
}
|
|
60
|
+
/** Narrow an untrusted string to a {@link FeatureSettingsEffect}, defaulting to `inherit`. */
|
|
61
|
+
export declare function toFeatureSettingsEffect(value: unknown): FeatureSettingsEffect;
|
|
62
|
+
/** Label for the "back to default" option, naming the state it returns to. */
|
|
63
|
+
export declare function defaultOptionLabel(enabledWhenDefault: boolean): string;
|
|
64
|
+
/**
|
|
65
|
+
* Label for the option that removes the **tenant** override.
|
|
66
|
+
*
|
|
67
|
+
* `defaultEnabled` is the wrong thing to name here: the tenant scope inherits
|
|
68
|
+
* whatever the levels above it resolve to — the global override, and, when a
|
|
69
|
+
* tenant hierarchy is configured, any ancestor tenant's override. So this names
|
|
70
|
+
* the inherited state only when the row carries one, and otherwise says
|
|
71
|
+
* "Default (inherited)" rather than claiming a state it cannot know.
|
|
72
|
+
*/
|
|
73
|
+
export declare function inheritedOptionLabel(feature: FeatureSettingsView): string;
|
|
74
|
+
/** Badge text for a feature's effective state. */
|
|
75
|
+
export declare function effectiveStateLabel(effectiveEnabled: boolean): string;
|
|
76
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/svelte/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,qFAAqF;AACrF,MAAM,MAAM,qBAAqB,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAErE,qDAAqD;AACrD,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,cAAc,EAAE,OAAO,CAAC;IACxB,yDAAyD;IACzD,gBAAgB,EAAE,OAAO,CAAC;IAC1B,8EAA8E;IAC9E,YAAY,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAC5C,8EAA8E;IAC9E,YAAY,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAC5C;;;;;;;;;OASG;IACH,gBAAgB,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC;AAED,oDAAoD;AACpD,MAAM,WAAW,qBAAqB;IACpC,4CAA4C;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,qCAAqC;IACrC,YAAY,EAAE,qBAAqB,CAAC;IACpC;;;OAGG;IACH,YAAY,CAAC,EAAE,qBAAqB,CAAC;CACtC;AAED,8FAA8F;AAC9F,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,CAE7E;AAED,8EAA8E;AAC9E,wBAAgB,kBAAkB,CAAC,kBAAkB,EAAE,OAAO,GAAG,MAAM,CAEtE;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAIzE;AAED,kDAAkD;AAClD,wBAAgB,mBAAmB,CAAC,gBAAgB,EAAE,OAAO,GAAG,MAAM,CAErE"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentational types for the feature-flag management UI.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately free of the `@smrt()` model classes and of `@happyvertical/smrt-core`
|
|
5
|
+
* so the Svelte bundle stays a thin presentation layer. The effect strings match
|
|
6
|
+
* `FeatureOverrideEffect`'s values, so a `FeatureSettingsRow` produced by
|
|
7
|
+
* `FeatureSettingsService.listFeatureSettings()` is structurally assignable to
|
|
8
|
+
* {@link FeatureSettingsView} with no mapping step.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/** Narrow an untrusted string to a {@link FeatureSettingsEffect}, defaulting to `inherit`. */
|
|
13
|
+
export function toFeatureSettingsEffect(value) {
|
|
14
|
+
return value === 'enable' || value === 'disable' ? value : 'inherit';
|
|
15
|
+
}
|
|
16
|
+
/** Label for the "back to default" option, naming the state it returns to. */
|
|
17
|
+
export function defaultOptionLabel(enabledWhenDefault) {
|
|
18
|
+
return `Default (${enabledWhenDefault ? 'enabled' : 'disabled'})`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Label for the option that removes the **tenant** override.
|
|
22
|
+
*
|
|
23
|
+
* `defaultEnabled` is the wrong thing to name here: the tenant scope inherits
|
|
24
|
+
* whatever the levels above it resolve to — the global override, and, when a
|
|
25
|
+
* tenant hierarchy is configured, any ancestor tenant's override. So this names
|
|
26
|
+
* the inherited state only when the row carries one, and otherwise says
|
|
27
|
+
* "Default (inherited)" rather than claiming a state it cannot know.
|
|
28
|
+
*/
|
|
29
|
+
export function inheritedOptionLabel(feature) {
|
|
30
|
+
return typeof feature.inheritedEnabled === 'boolean'
|
|
31
|
+
? defaultOptionLabel(feature.inheritedEnabled)
|
|
32
|
+
: 'Default (inherited)';
|
|
33
|
+
}
|
|
34
|
+
/** Badge text for a feature's effective state. */
|
|
35
|
+
export function effectiveStateLabel(effectiveEnabled) {
|
|
36
|
+
return effectiveEnabled ? 'Enabled' : 'Disabled';
|
|
37
|
+
}
|