@svadmin/surface 0.14.2 → 0.16.0
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/compatibility.json +1 -1
- package/dist/builtin-schemas.d.ts +2 -2
- package/dist/business-definitions.d.ts +2 -2
- package/dist/components/ActivityFeedWidget.svelte +2 -2
- package/dist/components/BarChartWidget.svelte +2 -2
- package/dist/components/LineChartWidget.svelte +2 -2
- package/dist/components/MetricWidget.svelte +2 -2
- package/dist/components/ResourceDetailWidget.svelte +2 -2
- package/dist/components/ResourceTableWidget.svelte +2 -2
- package/dist/components/SurfaceRenderer.svelte +33 -8
- package/dist/components/SvarGridWidget.svelte +3 -3
- package/dist/json-render.js +1 -1
- package/dist/localization.d.ts +4 -0
- package/dist/localization.js +11 -0
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +9 -4
- package/dist/source-cache.d.ts +6 -3
- package/dist/source-cache.js +26 -3
- package/dist/workflows/catalog.d.ts +1 -1
- package/package.json +4 -4
package/compatibility.json
CHANGED
|
@@ -37,13 +37,13 @@ export type ResourceTableProps = Static<typeof resourceTablePropsSchema>;
|
|
|
37
37
|
export type BarChartProps = Static<typeof barChartPropsSchema>;
|
|
38
38
|
export type LineChartProps = Static<typeof lineChartPropsSchema>;
|
|
39
39
|
export declare const styledMetricPropsSchema: Type.TUnion<[Type.TObject<{
|
|
40
|
-
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "
|
|
40
|
+
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "danger" | "neutral">[]>>;
|
|
41
41
|
label: Type.TString;
|
|
42
42
|
format: Type.TLiteral<"currency">;
|
|
43
43
|
currency: Type.TString;
|
|
44
44
|
description: Type.TOptional<Type.TString>;
|
|
45
45
|
}>, Type.TObject<{
|
|
46
|
-
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "
|
|
46
|
+
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "danger" | "neutral">[]>>;
|
|
47
47
|
label: Type.TString;
|
|
48
48
|
format: Type.TUnion<[Type.TLiteral<"number">, Type.TLiteral<"percent">]>;
|
|
49
49
|
currency: Type.TOptional<Type.TNever>;
|
|
@@ -7,7 +7,7 @@ export declare const resourceDetailPropsSchema: Type.TObject<{
|
|
|
7
7
|
format: Type.TOptional<Type.TUnion<Type.TLiteral<string>[]>>;
|
|
8
8
|
}>>;
|
|
9
9
|
title: Type.TString;
|
|
10
|
-
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "
|
|
10
|
+
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "danger" | "neutral">[]>>;
|
|
11
11
|
}>;
|
|
12
12
|
export declare const activityFeedPropsSchema: Type.TObject<{
|
|
13
13
|
idField: Type.TString;
|
|
@@ -18,7 +18,7 @@ export declare const activityFeedPropsSchema: Type.TObject<{
|
|
|
18
18
|
commentField: Type.TOptional<Type.TString>;
|
|
19
19
|
statusField: Type.TOptional<Type.TString>;
|
|
20
20
|
title: Type.TString;
|
|
21
|
-
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "
|
|
21
|
+
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "danger" | "neutral">[]>>;
|
|
22
22
|
}>;
|
|
23
23
|
export type ResourceDetailProps = Static<typeof resourceDetailPropsSchema>;
|
|
24
24
|
export type ActivityFeedProps = Static<typeof activityFeedPropsSchema>;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { asSurfaceActivities } from '../business-data.js';
|
|
6
6
|
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
7
7
|
import { surfaceMetric } from '../recipes.js';
|
|
8
|
-
import { formatSurfaceMessage, resolveSurfaceMessages } from '../localization.js';
|
|
8
|
+
import { formatSurfaceMessage, resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
9
9
|
|
|
10
10
|
let { props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
11
11
|
const baseProps = $derived(Object.fromEntries(Object.entries(props).filter(([key]) => key !== 'appearance')));
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
{:else if data.status === 'empty'}
|
|
24
24
|
<p role="status">{text.activityEmpty}</p>
|
|
25
25
|
{:else if data.status === 'error'}
|
|
26
|
-
<p role="alert">{text.activityUnavailable}</p>
|
|
26
|
+
<p role="alert">{surfaceErrorMessage(data.error, text, text.activityUnavailable, text.activityInvalidData)}</p>
|
|
27
27
|
{:else if activities?.ok}
|
|
28
28
|
<ActivityFeed activities={activities.value} allowComment={false} title={p.title} emptyLabel={text.activityEmpty}
|
|
29
29
|
countLabel={formatSurfaceMessage(text.activityCount, { count: activities.value.length })} />
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import Card from '@svadmin/ui/components/ui/card/card.svelte';
|
|
8
8
|
import { barChartPropsSchema } from '../builtin-schemas.js';
|
|
9
9
|
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
10
|
-
import { formatSurfaceMessage, resolveSurfaceMessages } from '../localization.js';
|
|
10
|
+
import { formatSurfaceMessage, resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
11
11
|
import { asChartPoints, compactChartLabel } from '../widget-data.js';
|
|
12
12
|
|
|
13
13
|
let { props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
{:else if data.status === 'empty'}
|
|
33
33
|
<p class="chart-state" role="status">{activeMessages.chartNoData}</p>
|
|
34
34
|
{:else if data.status === 'error'}
|
|
35
|
-
<p class="chart-state" role="alert">{data.error.
|
|
35
|
+
<p class="chart-state" role="alert">{surfaceErrorMessage(data.error, activeMessages, activeMessages.chartUnavailable, activeMessages.chartInvalidData)}</p>
|
|
36
36
|
{:else if data.status === 'ready' && points?.ok}
|
|
37
37
|
<div class="chart-frame" role="img" aria-label={chartProps.title}>
|
|
38
38
|
<BarChart data={chartData} showValues={chartProps.showValues ?? true} />
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import Card from '@svadmin/ui/components/ui/card/card.svelte';
|
|
8
8
|
import { lineChartPropsSchema } from '../builtin-schemas.js';
|
|
9
9
|
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
10
|
-
import { formatSurfaceMessage, resolveSurfaceMessages } from '../localization.js';
|
|
10
|
+
import { formatSurfaceMessage, resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
11
11
|
import { asChartPoints, compactChartLabel } from '../widget-data.js';
|
|
12
12
|
|
|
13
13
|
let { props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
{:else if data.status === 'empty'}
|
|
33
33
|
<p class="chart-state" role="status">{activeMessages.chartNoData}</p>
|
|
34
34
|
{:else if data.status === 'error'}
|
|
35
|
-
<p class="chart-state" role="alert">{data.error.
|
|
35
|
+
<p class="chart-state" role="alert">{surfaceErrorMessage(data.error, activeMessages, activeMessages.chartUnavailable, activeMessages.chartInvalidData)}</p>
|
|
36
36
|
{:else if data.status === 'ready' && points?.ok}
|
|
37
37
|
<div class="chart-frame" role="img" aria-label={chartProps.title}>
|
|
38
38
|
<LineChart
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import StatsCard from '@svadmin/ui/components/StatsCard.svelte';
|
|
6
6
|
import { styledMetricPropsSchema as metricPropsSchema } from '../builtin-schemas.js';
|
|
7
7
|
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
8
|
-
import { resolveSurfaceMessages } from '../localization.js';
|
|
8
|
+
import { resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
9
9
|
|
|
10
10
|
let { widgetId, props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
11
11
|
const activeMessages = $derived(resolveSurfaceMessages(locale, messages));
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
{:else}
|
|
43
43
|
<div class={"metric-state " + (metricClasses?.state ?? "")} role={data.status === 'error' ? 'alert' : 'status'}>
|
|
44
44
|
<strong>{metricProps.label}</strong>
|
|
45
|
-
<span>{data.status === 'error' ? data.error.
|
|
45
|
+
<span>{data.status === 'error' ? surfaceErrorMessage(data.error, activeMessages, activeMessages.providerUnavailable, activeMessages.metricInvalidData) : data.status === 'empty' ? activeMessages.metricNoData : activeMessages.metricInvalidData}</span>
|
|
46
46
|
</div>
|
|
47
47
|
{/if}
|
|
48
48
|
</article>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
9
9
|
import type { JsonObject } from '../types.js';
|
|
10
10
|
import { surfaceMetric } from '../recipes.js';
|
|
11
|
-
import { resolveSurfaceMessages } from '../localization.js';
|
|
11
|
+
import { resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
12
12
|
import { displayTableValue } from '../widget-data.js';
|
|
13
13
|
|
|
14
14
|
let { props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
{:else if data.status === 'empty'}
|
|
31
31
|
<p role="status">{text.detailEmpty}</p>
|
|
32
32
|
{:else if data.status === 'error'}
|
|
33
|
-
<p role="alert">{text.detailUnavailable}</p>
|
|
33
|
+
<p role="alert">{surfaceErrorMessage(data.error, text, text.detailUnavailable, text.detailInvalidData)}</p>
|
|
34
34
|
{:else if record}
|
|
35
35
|
<dl aria-label={p.title}>
|
|
36
36
|
{#each p.fields as item, index (`${index}:${item.field}`)}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import Table from '@svadmin/ui/components/ui/table/table.svelte';
|
|
15
15
|
import { styledResourceTablePropsSchema as resourceTablePropsSchema } from '../builtin-schemas.js';
|
|
16
16
|
import type { SurfaceWidgetRendererProps } from '../catalog.js';
|
|
17
|
-
import { resolveSurfaceMessages } from '../localization.js';
|
|
17
|
+
import { resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
18
18
|
import { asRecordArray, displayTableValue } from '../widget-data.js';
|
|
19
19
|
|
|
20
20
|
let { props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
{:else if data.status === 'empty'}
|
|
36
36
|
<p class={"table-state " + (tableClasses?.state ?? "")} role="status">{tableProps.emptyLabel ?? activeMessages.tableNoRecords}</p>
|
|
37
37
|
{:else if data.status === 'error'}
|
|
38
|
-
<p class={"table-state " + (tableClasses?.state ?? "")} role="alert">{data.error.
|
|
38
|
+
<p class={"table-state " + (tableClasses?.state ?? "")} role="alert">{surfaceErrorMessage(data.error, activeMessages, activeMessages.tableUnavailable, activeMessages.tableInvalidData)}</p>
|
|
39
39
|
{:else if data.status === 'ready' && records?.ok}
|
|
40
40
|
<div class="surface-table">
|
|
41
41
|
<Table aria-label={tableProps.title}>
|
|
@@ -69,14 +69,26 @@
|
|
|
69
69
|
]);
|
|
70
70
|
// 复用 PR #428 的请求所有权与会话隔离实现,不维护第二套缓存。
|
|
71
71
|
const sourceCache = createSurfaceSourceCache({
|
|
72
|
-
set: (id, state) => {
|
|
72
|
+
set: (id, state) => {
|
|
73
|
+
if (state.status !== 'error') {
|
|
74
|
+
sourceStates.set(id, state);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const message = state.error.code === 'access_denied'
|
|
78
|
+
? activeMessages.accessDenied
|
|
79
|
+
: state.error.code === 'provider_result_not_json' || state.error.code === 'binding_pointer_not_found'
|
|
80
|
+
? state.error.message
|
|
81
|
+
: activeMessages.providerUnavailable;
|
|
82
|
+
sourceStates.set(id, { ...state, error: { ...state.error, message } });
|
|
83
|
+
},
|
|
73
84
|
remove: (id) => { sourceStates.delete(id); },
|
|
74
|
-
onError: (state) => { onError?.({ type: 'data', error: state.error }); },
|
|
85
|
+
onError: (state, diagnostic) => { onError?.({ type: 'data', error: diagnostic ?? state.error }); },
|
|
75
86
|
});
|
|
76
87
|
|
|
77
|
-
function providerError(sourceId: string
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
function providerError(sourceId: string): SurfaceSourceDataState {
|
|
89
|
+
return { status: 'error', sourceId, error: {
|
|
90
|
+
code: 'provider_failed', sourceId, message: activeMessages.providerUnavailable,
|
|
91
|
+
} };
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
const sourceRequests: SurfaceSourceRequest[] = $derived.by(() => {
|
|
@@ -97,11 +109,18 @@
|
|
|
97
109
|
request.id === snapshot.source.id && sameSourceIdentity(request.identity, identity));
|
|
98
110
|
return {
|
|
99
111
|
id: snapshot.source.id, identity, isCurrent,
|
|
100
|
-
async load(isRequestCurrent) {
|
|
101
|
-
if (!provider)
|
|
112
|
+
async load(isRequestCurrent, reportDiagnostic) {
|
|
113
|
+
if (!provider) {
|
|
114
|
+
reportDiagnostic?.({
|
|
115
|
+
code: 'provider_failed', sourceId: snapshot.source.id,
|
|
116
|
+
message: providerFailure instanceof Error ? providerFailure.message : 'Data provider request failed',
|
|
117
|
+
});
|
|
118
|
+
return providerError(snapshot.source.id);
|
|
119
|
+
}
|
|
102
120
|
try {
|
|
103
121
|
return await loadSurfaceSource({
|
|
104
122
|
source: snapshot.source, resourcePolicy: snapshot.resourcePolicy, provider,
|
|
123
|
+
...(reportDiagnostic === undefined ? {} : { reportDiagnostic }),
|
|
105
124
|
async authorize(resource, action) {
|
|
106
125
|
if (!isRequestCurrent()) return { can: false };
|
|
107
126
|
// 授权检查与缓存身份必须使用同一个所属上下文,不能回退到无关全局授权。
|
|
@@ -112,7 +131,13 @@
|
|
|
112
131
|
return isRequestCurrent() ? decision : { can: false };
|
|
113
132
|
},
|
|
114
133
|
});
|
|
115
|
-
} catch (failure) {
|
|
134
|
+
} catch (failure) {
|
|
135
|
+
reportDiagnostic?.({
|
|
136
|
+
code: 'provider_failed', sourceId: snapshot.source.id,
|
|
137
|
+
message: failure instanceof Error ? failure.message : 'Data provider request failed',
|
|
138
|
+
});
|
|
139
|
+
return providerError(snapshot.source.id);
|
|
140
|
+
}
|
|
116
141
|
},
|
|
117
142
|
};
|
|
118
143
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { SVAR_SURFACE_CONTEXT, type SvarSurfaceContext } from '../svar-context.js';
|
|
6
6
|
import { decodeSvarGridProps } from '../svar-schema.js';
|
|
7
7
|
import { asRecordArray } from '../widget-data.js';
|
|
8
|
-
import { resolveSurfaceMessages } from '../localization.js';
|
|
8
|
+
import { resolveSurfaceMessages, surfaceErrorMessage } from '../localization.js';
|
|
9
9
|
|
|
10
10
|
let { widgetId, props, data, locale = 'en-US', messages }: SurfaceWidgetRendererProps = $props();
|
|
11
11
|
const host = getContext<SvarSurfaceContext | undefined>(SVAR_SURFACE_CONTEXT);
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
<section aria-labelledby="svar-surface-{widgetId}" class="surface-data-grid">
|
|
25
25
|
<h3 id="svar-surface-{widgetId}">{config.title}</h3>
|
|
26
26
|
{#if !Grid || !Theme}
|
|
27
|
-
<p role="alert">{
|
|
27
|
+
<p role="alert">{labels.tableEngineUnavailable}</p>
|
|
28
28
|
{:else if data.status === 'error'}
|
|
29
|
-
<p role="alert">{data.error.
|
|
29
|
+
<p role="alert">{surfaceErrorMessage(data.error, labels, labels.tableUnavailable, labels.tableInvalidData)}</p>
|
|
30
30
|
{:else if data.status === 'unbound' || (records && !records.ok)}
|
|
31
31
|
<p role="alert">{labels.tableInvalidData}</p>
|
|
32
32
|
{:else}
|
package/dist/json-render.js
CHANGED
|
@@ -71,7 +71,7 @@ export function jsonRenderSpecToSurfaceSpec(input, catalog, policy) {
|
|
|
71
71
|
if (!element)
|
|
72
72
|
return { ok: false, issues: [issue(jsonPointer(['elements', childId]), 'Missing child element')] };
|
|
73
73
|
const widget = readWidgetElement(element, childId);
|
|
74
|
-
if (
|
|
74
|
+
if (typeof widget.error === 'string')
|
|
75
75
|
return { ok: false, issues: [issue(jsonPointer(['elements', childId]), widget.error)] };
|
|
76
76
|
widgets.push(widget);
|
|
77
77
|
}
|
package/dist/localization.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { SurfaceDataError } from './types.js';
|
|
1
2
|
export interface SurfaceMessages {
|
|
2
3
|
readonly renderErrorTitle: string;
|
|
3
4
|
readonly providerUnavailable: string;
|
|
5
|
+
readonly accessDenied?: string;
|
|
4
6
|
readonly metricNoData: string;
|
|
5
7
|
readonly metricInvalidData: string;
|
|
6
8
|
readonly chartLoading: string;
|
|
@@ -11,6 +13,7 @@ export interface SurfaceMessages {
|
|
|
11
13
|
readonly tableNoRecords: string;
|
|
12
14
|
readonly tableInvalidData: string;
|
|
13
15
|
readonly tableUnavailable: string;
|
|
16
|
+
readonly tableEngineUnavailable: string;
|
|
14
17
|
readonly detailLoading?: string;
|
|
15
18
|
readonly detailEmpty?: string;
|
|
16
19
|
readonly detailUnavailable?: string;
|
|
@@ -26,3 +29,4 @@ export interface SurfaceMessages {
|
|
|
26
29
|
}
|
|
27
30
|
export declare function resolveSurfaceMessages(locale: string, overrides?: Partial<SurfaceMessages>): Required<SurfaceMessages>;
|
|
28
31
|
export declare function formatSurfaceMessage(template: string, params: Readonly<Record<string, string | number>>): string;
|
|
32
|
+
export declare function surfaceErrorMessage(error: Pick<SurfaceDataError, 'code'>, messages: Required<SurfaceMessages>, unavailable: string, invalid: string): string;
|
package/dist/localization.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const englishMessages = {
|
|
2
2
|
renderErrorTitle: 'Surface could not be rendered',
|
|
3
3
|
providerUnavailable: 'Data provider is unavailable',
|
|
4
|
+
accessDenied: 'Access denied',
|
|
4
5
|
metricNoData: 'No data',
|
|
5
6
|
metricInvalidData: 'Metric data is invalid',
|
|
6
7
|
chartLoading: 'Loading {title}',
|
|
@@ -11,6 +12,7 @@ const englishMessages = {
|
|
|
11
12
|
tableNoRecords: 'No records',
|
|
12
13
|
tableInvalidData: 'Table data is invalid',
|
|
13
14
|
tableUnavailable: 'Table data is unavailable',
|
|
15
|
+
tableEngineUnavailable: 'The host has not configured the SVAR engine',
|
|
14
16
|
detailLoading: 'Loading record',
|
|
15
17
|
detailEmpty: 'No record',
|
|
16
18
|
detailUnavailable: 'Record is unavailable',
|
|
@@ -27,6 +29,7 @@ const englishMessages = {
|
|
|
27
29
|
const simplifiedChineseMessages = {
|
|
28
30
|
renderErrorTitle: '无法渲染 Surface',
|
|
29
31
|
providerUnavailable: '数据提供器不可用',
|
|
32
|
+
accessDenied: '无权访问',
|
|
30
33
|
metricNoData: '暂无数据',
|
|
31
34
|
metricInvalidData: '指标数据无效',
|
|
32
35
|
chartLoading: '正在加载{title}',
|
|
@@ -37,6 +40,7 @@ const simplifiedChineseMessages = {
|
|
|
37
40
|
tableNoRecords: '暂无记录',
|
|
38
41
|
tableInvalidData: '表格数据无效',
|
|
39
42
|
tableUnavailable: '表格数据不可用',
|
|
43
|
+
tableEngineUnavailable: '宿主尚未配置 SVAR 引擎',
|
|
40
44
|
detailLoading: '正在加载记录',
|
|
41
45
|
detailEmpty: '暂无记录',
|
|
42
46
|
detailUnavailable: '记录不可用',
|
|
@@ -63,3 +67,10 @@ export function formatSurfaceMessage(template, params) {
|
|
|
63
67
|
}
|
|
64
68
|
return formattedMessage;
|
|
65
69
|
}
|
|
70
|
+
export function surfaceErrorMessage(error, messages, unavailable, invalid) {
|
|
71
|
+
if (error.code === 'access_denied')
|
|
72
|
+
return messages.accessDenied;
|
|
73
|
+
if (error.code === 'provider_result_not_json' || error.code === 'binding_pointer_not_found')
|
|
74
|
+
return invalid;
|
|
75
|
+
return unavailable;
|
|
76
|
+
}
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SurfaceDataProvider, SurfaceDataSource, SurfaceResourcePolicy, SurfaceWidgetDataState } from './types.js';
|
|
1
|
+
import type { SurfaceDataError, SurfaceDataProvider, SurfaceDataSource, SurfaceResourcePolicy, SurfaceWidgetDataState } from './types.js';
|
|
2
2
|
export type SurfaceReadAction = 'list' | 'show';
|
|
3
3
|
export interface SurfaceAccessDecision {
|
|
4
4
|
readonly can: boolean;
|
|
@@ -10,6 +10,8 @@ export interface LoadSurfaceSourceRequest {
|
|
|
10
10
|
readonly resourcePolicy: SurfaceResourcePolicy;
|
|
11
11
|
readonly provider: SurfaceDataProvider;
|
|
12
12
|
readonly authorize: SurfaceAuthorizer;
|
|
13
|
+
/** Internal host hook for diagnostics; never used as user-visible state. */
|
|
14
|
+
readonly reportDiagnostic?: (diagnostic: SurfaceDataError) => void;
|
|
13
15
|
}
|
|
14
16
|
export type SurfaceLoadedSourceState = Extract<SurfaceWidgetDataState, {
|
|
15
17
|
status: 'ready' | 'error';
|
package/dist/runtime.js
CHANGED
|
@@ -46,6 +46,10 @@ function projectRecords(records, readableFields) {
|
|
|
46
46
|
function sourceError(sourceId, code, message) {
|
|
47
47
|
return { status: 'error', sourceId, error: { code, sourceId, message } };
|
|
48
48
|
}
|
|
49
|
+
function reportedError(request, code, message, diagnosticMessage) {
|
|
50
|
+
request.reportDiagnostic?.({ code, sourceId: request.source.id, message: diagnosticMessage });
|
|
51
|
+
return sourceError(request.source.id, code, message);
|
|
52
|
+
}
|
|
49
53
|
function listParams(source) {
|
|
50
54
|
const sorters = source.sorters?.map((sorter) => ({ ...sorter })) ?? [];
|
|
51
55
|
const filters = source.filters?.map((filter) => {
|
|
@@ -112,10 +116,11 @@ export async function loadSurfaceSource(request) {
|
|
|
112
116
|
}
|
|
113
117
|
catch (accessFailure) {
|
|
114
118
|
const message = accessFailure instanceof Error ? accessFailure.message : 'Access check failed';
|
|
115
|
-
return
|
|
119
|
+
return reportedError(request, 'access_check_failed', 'Access check failed', message);
|
|
120
|
+
}
|
|
121
|
+
if (!accessDecision.can) {
|
|
122
|
+
return reportedError(request, 'access_denied', 'Access denied', accessDecision.reason ?? 'Access denied');
|
|
116
123
|
}
|
|
117
|
-
if (!accessDecision.can)
|
|
118
|
-
return sourceError(source.id, 'access_denied', accessDecision.reason ?? 'Access denied');
|
|
119
124
|
try {
|
|
120
125
|
return source.type === 'resource-list'
|
|
121
126
|
? await loadListSource(source, resourcePolicy, provider)
|
|
@@ -123,6 +128,6 @@ export async function loadSurfaceSource(request) {
|
|
|
123
128
|
}
|
|
124
129
|
catch (providerFailure) {
|
|
125
130
|
const message = providerFailure instanceof Error ? providerFailure.message : 'Data provider request failed';
|
|
126
|
-
return
|
|
131
|
+
return reportedError(request, 'provider_failed', 'Data provider request failed', message);
|
|
127
132
|
}
|
|
128
133
|
}
|
package/dist/source-cache.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type { SurfaceDataSource, SurfaceResourcePolicy, SurfaceSourceDataState } from './types.js';
|
|
1
|
+
import type { SurfaceDataSource, SurfaceDataError, SurfaceResourcePolicy, SurfaceSourceDataState } from './types.js';
|
|
2
|
+
type SurfaceDataDiagnostic = Extract<SurfaceSourceDataState, {
|
|
3
|
+
status: 'error';
|
|
4
|
+
}>['error'];
|
|
2
5
|
/** Only query/projection inputs belong in this key, never widget or layout data. */
|
|
3
6
|
export declare function snapshotSurfaceSource(source: SurfaceDataSource, policy: SurfaceResourcePolicy): {
|
|
4
7
|
source: SurfaceDataSource;
|
|
@@ -9,7 +12,7 @@ export interface SurfaceSourceRequest {
|
|
|
9
12
|
readonly id: string;
|
|
10
13
|
/** Values compare by Object.is; provider and permission identities are not serialized. */
|
|
11
14
|
readonly identity: readonly unknown[];
|
|
12
|
-
readonly load: (isCurrent: () => boolean) => Promise<SurfaceSourceDataState>;
|
|
15
|
+
readonly load: (isCurrent: () => boolean, reportDiagnostic?: (diagnostic: SurfaceDataDiagnostic) => void) => Promise<SurfaceSourceDataState>;
|
|
13
16
|
/** Also guards prop changes that happen before Svelte's next effect flush. */
|
|
14
17
|
readonly isCurrent: () => boolean;
|
|
15
18
|
}
|
|
@@ -19,7 +22,7 @@ interface SourceCacheSink {
|
|
|
19
22
|
readonly remove: (sourceId: string) => void;
|
|
20
23
|
readonly onError: (state: Extract<SurfaceSourceDataState, {
|
|
21
24
|
status: 'error';
|
|
22
|
-
}
|
|
25
|
+
}>, diagnostic?: SurfaceDataError) => void;
|
|
23
26
|
}
|
|
24
27
|
/** Per-renderer reconciliation, not a cross-user or cross-component data cache. */
|
|
25
28
|
export declare function createSurfaceSourceCache(sink: SourceCacheSink): {
|
package/dist/source-cache.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
function stableSourceState(state) {
|
|
2
|
+
if (state.status !== 'error')
|
|
3
|
+
return state;
|
|
4
|
+
const message = state.error.code === 'access_denied'
|
|
5
|
+
? 'Access denied'
|
|
6
|
+
: state.error.code === 'access_check_failed'
|
|
7
|
+
? 'Access check failed'
|
|
8
|
+
: state.error.code === 'provider_failed'
|
|
9
|
+
? 'Data provider request failed'
|
|
10
|
+
: state.error.message;
|
|
11
|
+
return message === state.error.message
|
|
12
|
+
? state
|
|
13
|
+
: { ...state, error: { ...state.error, message } };
|
|
14
|
+
}
|
|
1
15
|
/** Only query/projection inputs belong in this key, never widget or layout data. */
|
|
2
16
|
export function snapshotSurfaceSource(source, policy) {
|
|
3
17
|
const snapshot = source.type === 'resource-one'
|
|
@@ -79,15 +93,23 @@ export function createSurfaceSourceCache(sink) {
|
|
|
79
93
|
return;
|
|
80
94
|
}
|
|
81
95
|
let state;
|
|
96
|
+
let diagnostic;
|
|
97
|
+
const reportDiagnostic = (next) => {
|
|
98
|
+
diagnostic ??= next;
|
|
99
|
+
};
|
|
82
100
|
try {
|
|
83
|
-
state = await request.load(current);
|
|
101
|
+
state = await request.load(current, reportDiagnostic);
|
|
84
102
|
}
|
|
85
103
|
catch (failure) {
|
|
104
|
+
const message = failure instanceof Error ? failure.message : 'Data provider request failed';
|
|
105
|
+
diagnostic = {
|
|
106
|
+
code: 'provider_failed', sourceId: request.id, message,
|
|
107
|
+
};
|
|
86
108
|
state = {
|
|
87
109
|
status: 'error', sourceId: request.id,
|
|
88
110
|
error: {
|
|
89
111
|
code: 'provider_failed', sourceId: request.id,
|
|
90
|
-
message:
|
|
112
|
+
message: 'Data provider request failed',
|
|
91
113
|
},
|
|
92
114
|
};
|
|
93
115
|
}
|
|
@@ -95,9 +117,10 @@ export function createSurfaceSourceCache(sink) {
|
|
|
95
117
|
retire();
|
|
96
118
|
return;
|
|
97
119
|
}
|
|
120
|
+
state = stableSourceState(state);
|
|
98
121
|
sink.set(request.id, state);
|
|
99
122
|
if (state.status === 'error')
|
|
100
|
-
sink.onError(state);
|
|
123
|
+
sink.onError(state, diagnostic);
|
|
101
124
|
})();
|
|
102
125
|
return entry.pending;
|
|
103
126
|
}
|
|
@@ -2,7 +2,7 @@ import { Type } from 'typebox';
|
|
|
2
2
|
import type { SurfaceCatalog, SurfaceWidgetDefinition, JsonObject } from '../types.js';
|
|
3
3
|
import type { SurfaceActionDescriptor } from './types.js';
|
|
4
4
|
export declare const surfaceAppearanceSchema: Type.TObject<{
|
|
5
|
-
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "
|
|
5
|
+
tone: Type.TOptional<Type.TUnion<Type.TLiteral<"info" | "warning" | "success" | "danger" | "neutral">[]>>;
|
|
6
6
|
}>;
|
|
7
7
|
export declare function withoutSurfaceAppearance(props: JsonObject): JsonObject;
|
|
8
8
|
/** Adds frame-level semantic appearance to every registered widget without
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/surface",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Validated declarative surfaces for svadmin dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -74,8 +74,8 @@
|
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@svadmin/core": ">=0.53.0 <0.
|
|
78
|
-
"@svadmin/ui": ">=0.73.0 <0.
|
|
77
|
+
"@svadmin/core": ">=0.53.0 <0.66.0",
|
|
78
|
+
"@svadmin/ui": ">=0.73.0 <0.92.0",
|
|
79
79
|
"svelte": "^5.56.10"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"@testing-library/dom": "^10.4.1",
|
|
87
87
|
"@testing-library/svelte": "^5.4.2",
|
|
88
88
|
"@testing-library/user-event": "^14.6.6",
|
|
89
|
-
"happy-dom": "^
|
|
89
|
+
"happy-dom": "^21.0.4",
|
|
90
90
|
"postcss": "^8.5.26",
|
|
91
91
|
"vitest": "^4.1.11"
|
|
92
92
|
},
|