@cosmicdrift/kumiko-renderer 0.389.0 → 0.390.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/dist/app/projection-detail-body.js +59 -394
- package/dist/app/projection-detail-header-model.d.ts +11 -0
- package/dist/app/projection-detail-header-model.js +33 -0
- package/dist/app/projection-detail-header-region.d.ts +28 -0
- package/dist/app/projection-detail-header-region.js +81 -0
- package/dist/app/projection-detail-header.d.ts +29 -0
- package/dist/app/projection-detail-header.js +84 -0
- package/dist/app/projection-detail-navigation.d.ts +13 -0
- package/dist/app/projection-detail-navigation.js +91 -0
- package/dist/app/projection-detail-states.d.ts +22 -0
- package/dist/app/projection-detail-states.js +26 -0
- package/dist/app/projection-drawer-host.d.ts +12 -0
- package/dist/app/projection-drawer-host.js +10 -0
- package/dist/app/projection-list-body.d.ts +1 -1
- package/dist/app/projection-list-body.js +69 -189
- package/dist/app/projection-list-render-props.d.ts +56 -0
- package/dist/app/projection-list-render-props.js +47 -0
- package/dist/app/projection-list-render.d.ts +26 -0
- package/dist/app/projection-list-render.js +20 -0
- package/dist/app/projection-list-row-click.d.ts +12 -0
- package/dist/app/projection-list-row-click.js +14 -0
- package/dist/app/use-projection-detail-actions.d.ts +35 -0
- package/dist/app/use-projection-detail-actions.js +156 -0
- package/dist/app/use-projection-detail-data.d.ts +19 -0
- package/dist/app/use-projection-detail-data.js +47 -0
- package/dist/app/use-projection-detail-layout.d.ts +17 -0
- package/dist/app/use-projection-detail-layout.js +23 -0
- package/dist/app/use-projection-list-actions.d.ts +31 -0
- package/dist/app/use-projection-list-actions.js +73 -0
- package/dist/app/use-projection-list-data.d.ts +20 -0
- package/dist/app/use-projection-list-data.js +26 -0
- package/dist/app/use-projection-list-definitions.d.ts +11 -0
- package/dist/app/use-projection-list-definitions.js +20 -0
- package/dist/app/use-projection-list-query.d.ts +28 -0
- package/dist/app/use-projection-list-query.js +83 -0
- package/dist/components/render-edit-banners.d.ts +12 -0
- package/dist/components/render-edit-banners.js +90 -0
- package/dist/components/render-edit-draft-save-helpers.d.ts +19 -0
- package/dist/components/render-edit-draft-save-helpers.js +42 -0
- package/dist/components/render-edit-field-grid.d.ts +4 -0
- package/dist/components/render-edit-field-grid.js +40 -0
- package/dist/components/render-edit-fields-section.d.ts +4 -0
- package/dist/components/render-edit-fields-section.js +76 -0
- package/dist/components/render-edit-form-actions.d.ts +4 -0
- package/dist/components/render-edit-form-actions.js +66 -0
- package/dist/components/render-edit-header-actions.d.ts +11 -0
- package/dist/components/render-edit-header-actions.js +120 -0
- package/dist/components/render-edit-layout-flags.d.ts +29 -0
- package/dist/components/render-edit-layout-flags.js +106 -0
- package/dist/components/render-edit-layout.d.ts +4 -0
- package/dist/components/render-edit-layout.js +79 -0
- package/dist/components/render-edit-render-model.d.ts +63 -0
- package/dist/components/render-edit-render-model.js +14 -0
- package/dist/components/render-edit-section-kinds.d.ts +4 -0
- package/dist/components/render-edit-section-kinds.js +115 -0
- package/dist/components/render-edit-submit-result.d.ts +8 -0
- package/dist/components/render-edit-submit-result.js +77 -0
- package/dist/components/render-edit-submit.d.ts +7 -0
- package/dist/components/render-edit-submit.js +113 -0
- package/dist/components/render-edit-titles.d.ts +17 -0
- package/dist/components/render-edit-titles.js +99 -0
- package/dist/components/render-edit-wizard-navigation.d.ts +11 -0
- package/dist/components/render-edit-wizard-navigation.js +115 -0
- package/dist/components/render-edit.d.ts +2 -2
- package/dist/components/render-edit.js +63 -1128
- package/dist/components/render-field-choice-inputs.d.ts +7 -0
- package/dist/components/render-field-choice-inputs.js +52 -0
- package/dist/components/render-field-control.d.ts +12 -0
- package/dist/components/render-field-control.js +93 -0
- package/dist/components/render-field-display-text.d.ts +4 -0
- package/dist/components/render-field-display-text.js +48 -0
- package/dist/components/render-field-fill-action.d.ts +13 -0
- package/dist/components/render-field-fill-action.js +63 -0
- package/dist/components/render-field-input-shared.d.ts +39 -0
- package/dist/components/render-field-input-shared.js +20 -0
- package/dist/components/render-field-input.d.ts +3 -0
- package/dist/components/render-field-input.js +34 -0
- package/dist/components/render-field-meta.d.ts +6 -0
- package/dist/components/render-field-meta.js +51 -0
- package/dist/components/render-field-read-only.d.ts +11 -0
- package/dist/components/render-field-read-only.js +75 -0
- package/dist/components/render-field-reference.d.ts +15 -0
- package/dist/components/render-field-reference.js +103 -0
- package/dist/components/render-field-scalar-inputs.d.ts +10 -0
- package/dist/components/render-field-scalar-inputs.js +35 -0
- package/dist/components/render-field-text-inputs.d.ts +3 -0
- package/dist/components/render-field-text-inputs.js +34 -0
- package/dist/components/render-field-values.d.ts +18 -0
- package/dist/components/render-field-values.js +111 -0
- package/dist/components/render-field-write-only.d.ts +9 -0
- package/dist/components/render-field-write-only.js +29 -0
- package/dist/components/render-field.d.ts +2 -2
- package/dist/components/render-field.js +39 -670
- package/dist/components/render-list-header-slot.d.ts +5 -0
- package/dist/components/render-list-header-slot.js +30 -0
- package/dist/components/render-list-reference-bridge.d.ts +3 -0
- package/dist/components/render-list-reference-bridge.js +25 -0
- package/dist/components/render-list-reference-columns.d.ts +25 -0
- package/dist/components/render-list-reference-columns.js +137 -0
- package/dist/components/render-list-table-props.d.ts +52 -0
- package/dist/components/render-list-table-props.js +53 -0
- package/dist/components/render-list-toolbar-model.d.ts +42 -0
- package/dist/components/render-list-toolbar-model.js +82 -0
- package/dist/components/render-list-toolbar.d.ts +38 -0
- package/dist/components/render-list-toolbar.js +40 -0
- package/dist/components/render-list-types.d.ts +113 -0
- package/dist/components/render-list-types.js +1 -0
- package/dist/components/render-list.d.ts +4 -113
- package/dist/components/render-list.js +91 -307
- package/dist/components/use-reference-input-state.d.ts +20 -0
- package/dist/components/use-reference-input-state.js +85 -0
- package/dist/components/use-render-edit-controlled-sync.d.ts +3 -0
- package/dist/components/use-render-edit-controlled-sync.js +26 -0
- package/dist/components/use-render-edit-draft-save.d.ts +29 -0
- package/dist/components/use-render-edit-draft-save.js +141 -0
- package/dist/components/use-render-edit-form.d.ts +10 -0
- package/dist/components/use-render-edit-form.js +101 -0
- package/dist/components/use-render-edit-sections.d.ts +15 -0
- package/dist/components/use-render-edit-sections.js +100 -0
- package/dist/components/use-render-edit-state.d.ts +65 -0
- package/dist/components/use-render-edit-state.js +158 -0
- package/dist/components/use-render-edit-steps.d.ts +13 -0
- package/dist/components/use-render-edit-steps.js +52 -0
- package/dist/components/use-render-list-columns.d.ts +18 -0
- package/dist/components/use-render-list-columns.js +41 -0
- package/dist/components/use-render-list-search.d.ts +4 -0
- package/dist/components/use-render-list-search.js +50 -0
- package/dist/primitives.d.ts +1 -1
- package/dist/web/app/dashboard-body.d.ts +1 -1
- package/dist/web/app/dashboard-body.js +42 -246
- package/dist/web/app/dashboard-header.d.ts +34 -0
- package/dist/web/app/dashboard-header.js +58 -0
- package/dist/web/app/dashboard-screen-controls.d.ts +11 -0
- package/dist/web/app/dashboard-screen-controls.js +42 -0
- package/dist/web/app/dashboard-screen-tiles.d.ts +8 -0
- package/dist/web/app/dashboard-screen-tiles.js +42 -0
- package/dist/web/app/dashboard-visibility.d.ts +42 -0
- package/dist/web/app/dashboard-visibility.js +102 -0
- package/dist/web/app/use-dashboard-screen-state.d.ts +35 -0
- package/dist/web/app/use-dashboard-screen-state.js +69 -0
- package/dist/web/primitives/data-table/actions-column.js +6 -5
- package/dist/web/primitives/data-table/card-body.d.ts +6 -0
- package/dist/web/primitives/data-table/card-body.js +32 -0
- package/dist/web/primitives/data-table/card-row.d.ts +5 -0
- package/dist/web/primitives/data-table/card-row.js +31 -0
- package/dist/web/primitives/data-table/card-view.d.ts +3 -0
- package/dist/web/primitives/data-table/card-view.js +13 -0
- package/dist/web/primitives/data-table/column-resize-handle.js +3 -1
- package/dist/web/primitives/data-table/data-cell-props.d.ts +7 -0
- package/dist/web/primitives/data-table/data-cell-props.js +8 -0
- package/dist/web/primitives/data-table/row-render-parts.d.ts +8 -0
- package/dist/web/primitives/data-table/row-render-parts.js +35 -0
- package/dist/web/primitives/data-table/table-body.d.ts +3 -0
- package/dist/web/primitives/data-table/table-body.js +32 -0
- package/dist/web/primitives/data-table/table-content.d.ts +3 -0
- package/dist/web/primitives/data-table/table-content.js +41 -0
- package/dist/web/primitives/data-table/table-data-cell.d.ts +4 -0
- package/dist/web/primitives/data-table/table-data-cell.js +25 -0
- package/dist/web/primitives/data-table/table-header-row.d.ts +3 -0
- package/dist/web/primitives/data-table/table-header-row.js +21 -0
- package/dist/web/primitives/data-table/table-view.d.ts +3 -0
- package/dist/web/primitives/data-table/table-view.js +25 -0
- package/dist/web/primitives/data-table/toolbar.d.ts +6 -0
- package/dist/web/primitives/data-table/toolbar.js +53 -0
- package/dist/web/primitives/data-table/use-data-table-state.d.ts +44 -0
- package/dist/web/primitives/data-table/use-data-table-state.js +123 -0
- package/dist/web/primitives/data-table.d.ts +3 -4
- package/dist/web/primitives/data-table.js +32 -252
- package/dist/web/primitives/file-value-list.js +1 -1
- package/dist/web/primitives/form-footer-parts.d.ts +25 -0
- package/dist/web/primitives/form-footer-parts.js +55 -0
- package/dist/web/primitives/form.js +33 -43
- package/dist/web/primitives/input-choice-kinds.d.ts +4 -0
- package/dist/web/primitives/input-choice-kinds.js +67 -0
- package/dist/web/primitives/input-file-kinds.d.ts +3 -0
- package/dist/web/primitives/input-file-kinds.js +6 -0
- package/dist/web/primitives/input-number-kinds.d.ts +4 -0
- package/dist/web/primitives/input-number-kinds.js +16 -0
- package/dist/web/primitives/input-optional-props.d.ts +20 -0
- package/dist/web/primitives/input-optional-props.js +24 -0
- package/dist/web/primitives/input-select-presentation.d.ts +13 -0
- package/dist/web/primitives/input-select-presentation.js +19 -0
- package/dist/web/primitives/input-shared.d.ts +22 -0
- package/dist/web/primitives/input-shared.js +10 -0
- package/dist/web/primitives/input-temporal-kinds.d.ts +5 -0
- package/dist/web/primitives/input-temporal-kinds.js +10 -0
- package/dist/web/primitives/input-text-kinds.d.ts +7 -0
- package/dist/web/primitives/input-text-kinds.js +33 -0
- package/dist/web/primitives/input.js +35 -161
- package/dist/web/widgets/chart-data.d.ts +35 -0
- package/dist/web/widgets/chart-data.js +37 -0
- package/dist/web/widgets/chart-markers.d.ts +26 -0
- package/dist/web/widgets/chart-markers.js +64 -0
- package/dist/web/widgets/chart-parts.d.ts +20 -0
- package/dist/web/widgets/chart-parts.js +15 -0
- package/dist/web/widgets/charts.d.ts +5 -78
- package/dist/web/widgets/charts.js +7 -485
- package/dist/web/widgets/drawer-panel-class.d.ts +3 -0
- package/dist/web/widgets/drawer-panel-class.js +40 -0
- package/dist/web/widgets/drawer-resize-controls.d.ts +11 -0
- package/dist/web/widgets/drawer-resize-controls.js +12 -0
- package/dist/web/widgets/drawer-sizing.d.ts +19 -0
- package/dist/web/widgets/drawer-sizing.js +60 -0
- package/dist/web/widgets/drawer.js +17 -116
- package/dist/web/widgets/stacked-area-brush.d.ts +13 -0
- package/dist/web/widgets/stacked-area-brush.js +91 -0
- package/dist/web/widgets/stacked-area-chart.d.ts +6 -0
- package/dist/web/widgets/stacked-area-chart.js +45 -0
- package/dist/web/widgets/stacked-area-chrome.d.ts +7 -0
- package/dist/web/widgets/stacked-area-chrome.js +77 -0
- package/dist/web/widgets/stacked-area-geometry.d.ts +54 -0
- package/dist/web/widgets/stacked-area-geometry.js +83 -0
- package/dist/web/widgets/stacked-area-model.d.ts +11 -0
- package/dist/web/widgets/stacked-area-model.js +1 -0
- package/dist/web/widgets/stacked-area-plot.d.ts +3 -0
- package/dist/web/widgets/stacked-area-plot.js +51 -0
- package/dist/web/widgets/stacked-area-props.d.ts +83 -0
- package/dist/web/widgets/stacked-area-props.js +16 -0
- package/dist/web/widgets/stacked-area-svg.d.ts +3 -0
- package/dist/web/widgets/stacked-area-svg.js +38 -0
- package/dist/web/widgets/stacked-area-tooltip.d.ts +3 -0
- package/dist/web/widgets/stacked-area-tooltip.js +86 -0
- package/dist/web/widgets/use-drawer-resize.d.ts +22 -0
- package/dist/web/widgets/use-drawer-resize.js +52 -0
- package/dist/web/widgets/use-stacked-area-tooltip.d.ts +9 -0
- package/dist/web/widgets/use-stacked-area-tooltip.js +34 -0
- package/dist/web/widgets/use-stacked-area-window.d.ts +20 -0
- package/dist/web/widgets/use-stacked-area-window.js +76 -0
- package/package.json +10 -10
- package/src/changes.json +54 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { EntityDefinition, EntityListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { ListColumnViewModel, ListViewModel, Translate } from "../headless/index.ts";
|
|
3
|
+
import { type PublishedReferenceLookup } from "./render-list-reference-columns.ts";
|
|
4
|
+
type RenderListColumnsInput = {
|
|
5
|
+
readonly screen: EntityListScreenDefinition;
|
|
6
|
+
readonly entity: Pick<EntityDefinition, "fields" | "derivedFields">;
|
|
7
|
+
readonly rows: readonly Readonly<Record<string, unknown>>[];
|
|
8
|
+
readonly featureName: string;
|
|
9
|
+
readonly translate: Translate;
|
|
10
|
+
};
|
|
11
|
+
export declare function useRenderListColumns(input: RenderListColumnsInput): {
|
|
12
|
+
referenceColumns: import("./render-list-reference-columns.ts").ReferenceColumnSpec[];
|
|
13
|
+
handleLookupMap: (field: string, lookup: PublishedReferenceLookup) => void;
|
|
14
|
+
enrichedVm: Omit<ListViewModel, "columns"> & {
|
|
15
|
+
columns: readonly ListColumnViewModel[];
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { useReferenceLinkResolver } from "../app/reference-link.js";
|
|
3
|
+
import { computeListViewModel } from "../headless/index.js";
|
|
4
|
+
import { enrichReferenceColumns, selectReferenceColumns, } from "./render-list-reference-columns.js";
|
|
5
|
+
export function useRenderListColumns(input) {
|
|
6
|
+
const { screen, entity, rows, featureName, translate } = input;
|
|
7
|
+
const vm = useMemo(() => computeListViewModel({ screen, entity, rows, translate, featureName }), [screen, entity, rows, translate, featureName]);
|
|
8
|
+
// Reference-field eager loading through bridge components.
|
|
9
|
+
// The Hooks rule requires useReferenceLookup for each reference column,
|
|
10
|
+
// but the number of columns varies with the schema. Each column therefore
|
|
11
|
+
// mounts a ReferenceLookupBridge that publishes its map to local state
|
|
12
|
+
// through onMap. Each bridge makes a constant number of hook calls (one),
|
|
13
|
+
// and React manages its lifecycle when the schema changes via key.
|
|
14
|
+
const referenceColumns = useMemo(() => selectReferenceColumns(vm.columns, featureName), [vm.columns, featureName]);
|
|
15
|
+
const [referenceLookups, setReferenceLookups] = useState({});
|
|
16
|
+
const handleLookupMap = useCallback((field, lookup) => {
|
|
17
|
+
setReferenceLookups((prev) => {
|
|
18
|
+
// Skip the update when the lookup is identical. Otherwise the bridge
|
|
19
|
+
// would re-render and send the same map back until React entered
|
|
20
|
+
// an infinite loop.
|
|
21
|
+
const current = prev[field];
|
|
22
|
+
if (current?.map === lookup.map && current.complete === lookup.complete)
|
|
23
|
+
return prev;
|
|
24
|
+
return { ...prev, [field]: lookup };
|
|
25
|
+
});
|
|
26
|
+
}, []);
|
|
27
|
+
const resolveReferenceLink = useReferenceLinkResolver();
|
|
28
|
+
const enrichedColumns = useMemo(() => enrichReferenceColumns({
|
|
29
|
+
columns: vm.columns,
|
|
30
|
+
referenceColumns,
|
|
31
|
+
referenceLookups,
|
|
32
|
+
resolveReferenceLink,
|
|
33
|
+
featureName,
|
|
34
|
+
translate,
|
|
35
|
+
}), [vm.columns, referenceColumns, referenceLookups, resolveReferenceLink, featureName, translate]);
|
|
36
|
+
const enrichedVm = useMemo(() => ({
|
|
37
|
+
...vm,
|
|
38
|
+
columns: enrichedColumns,
|
|
39
|
+
}), [vm, enrichedColumns]);
|
|
40
|
+
return { referenceColumns, handleLookupMap, enrichedVm };
|
|
41
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
3
|
+
// Local search buffer + debounce. External changes (browser back,
|
|
4
|
+
// cross-component reset) are mirrored back; typing only fires
|
|
5
|
+
// onSearchChange after 300ms without further keystrokes.
|
|
6
|
+
export function useRenderListSearch(searchValue, onSearchChange) {
|
|
7
|
+
const [localQ, setLocalQ] = useState(searchValue ?? "");
|
|
8
|
+
// Sync during render, not in a useEffect: the passive effect also fires on
|
|
9
|
+
// mount and can flush after a keystroke landing in the same commit,
|
|
10
|
+
// resetting localQ and swallowing the input.
|
|
11
|
+
const [syncedSearchValue, setSyncedSearchValue] = useState(searchValue);
|
|
12
|
+
// The parent may echo our own debounced emits back late (router/URL round
|
|
13
|
+
// trip), possibly after a newer emit already went out; adopting such a stale
|
|
14
|
+
// echo would erase keys typed in between. All unechoed emits are kept, and a
|
|
15
|
+
// matching echo drops itself and every older one. The render phase only reads
|
|
16
|
+
// the list (a discarded or repeated render must find the echo again); the
|
|
17
|
+
// matched entries are dropped in an effect.
|
|
18
|
+
const pendingEmitsRef = useRef([]);
|
|
19
|
+
if (searchValue !== syncedSearchValue) {
|
|
20
|
+
setSyncedSearchValue(searchValue);
|
|
21
|
+
if (!pendingEmitsRef.current.includes(searchValue ?? "")) {
|
|
22
|
+
setLocalQ(searchValue ?? "");
|
|
23
|
+
pendingEmitsRef.current = [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const echoIndex = pendingEmitsRef.current.indexOf(syncedSearchValue ?? "");
|
|
28
|
+
if (echoIndex >= 0)
|
|
29
|
+
pendingEmitsRef.current.splice(0, echoIndex + 1);
|
|
30
|
+
}, [syncedSearchValue]);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
// skip: no search handler wired, so typing never emits
|
|
33
|
+
if (onSearchChange === undefined)
|
|
34
|
+
return;
|
|
35
|
+
// skip: buffer already matches the parent value
|
|
36
|
+
if (localQ === (searchValue ?? ""))
|
|
37
|
+
return;
|
|
38
|
+
// A late echo of an older emit changes searchValue but must not re-send
|
|
39
|
+
// the value that is already in flight.
|
|
40
|
+
// skip: that value is already in flight
|
|
41
|
+
if (pendingEmitsRef.current.at(-1) === localQ)
|
|
42
|
+
return;
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
pendingEmitsRef.current.push(localQ);
|
|
45
|
+
onSearchChange(localQ);
|
|
46
|
+
}, SEARCH_DEBOUNCE_MS);
|
|
47
|
+
return () => clearTimeout(timer);
|
|
48
|
+
}, [localQ, searchValue, onSearchChange]);
|
|
49
|
+
return { localQ, setLocalQ };
|
|
50
|
+
}
|
package/dist/primitives.d.ts
CHANGED
|
@@ -1365,7 +1365,7 @@ export type JsonViewProps = {
|
|
|
1365
1365
|
* PDFs, an inline preview. Used for `renderer: { format: "file" }` on a detail field. */
|
|
1366
1366
|
export type FileValueListProps = {
|
|
1367
1367
|
readonly fileIds: readonly string[];
|
|
1368
|
-
/** Starts the inline PDF preview of the
|
|
1368
|
+
/** Starts the inline PDF preview of the newest file open; uploads append, so that is the last id. */
|
|
1369
1369
|
readonly previewOpenByDefault?: boolean;
|
|
1370
1370
|
readonly testId?: string;
|
|
1371
1371
|
};
|
|
@@ -1,55 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { ErrorReference } from "../../app/error-reference.js";
|
|
4
|
-
import { dispatcherErrorText, EmbeddedScreenProvider, evalVisibleWhen, HeaderActionsBar, KumikoScreen, useEmbeddedScreen, useLocale, useNav, usePageHeaderCompact, usePageHeaderSlotAvailable, usePrimitives, useQuery, useRecordActions, useTranslation, } from "../../index.js";
|
|
5
|
-
import { cn } from "../lib/cn.js";
|
|
6
|
-
import { EmbeddedFormProvider } from "../primitives/index.js";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useNav, usePageHeaderCompact, usePageHeaderSlotAvailable, usePrimitives, useTranslation, } from "../../index.js";
|
|
7
3
|
import { PageSection } from "../primitives/layout.js";
|
|
8
|
-
import {
|
|
9
|
-
import { SectionCard } from "../widgets/section-card.js";
|
|
10
|
-
import { StatusBadge } from "../widgets/status-badge.js";
|
|
4
|
+
import { buildScopeBadge, dashboardDescription, recordTitleText, renderActionErrorBanner, renderDescriptionAndNotice, renderHeaderActionsBar, renderPageHeaderSlot, renderRecordError, renderRecordMissing, renderTitleRow, } from "./dashboard-header.js";
|
|
11
5
|
import { ChartPanelBody, CustomPanelBody, FeedPanelBody, ListPanelBody, ProgressListPanelBody, StatGroupPanelBody, StatPanelBody, TextPanelBody, } from "./dashboard-panels.js";
|
|
12
|
-
import {
|
|
13
|
-
import { DashboardQueryContext, HALF_PANEL,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// `params` can deep-link onto this filter.
|
|
17
|
-
function useFilterParams(screen, enabled) {
|
|
18
|
-
const { Field, Input } = usePrimitives();
|
|
19
|
-
const t = useTranslation();
|
|
20
|
-
const nav = useNav();
|
|
21
|
-
const filter = screen.filter;
|
|
22
|
-
const value = filter !== undefined ? (nav.searchParams[filter.id] ?? "") : "";
|
|
23
|
-
const optionsQueryResult = useQuery(filter?.optionsQuery ?? "", {}, { enabled: enabled && filter?.optionsQuery !== undefined });
|
|
24
|
-
if (filter === undefined) {
|
|
25
|
-
return { params: {}, picker: null };
|
|
26
|
-
}
|
|
27
|
-
const dynamicOptions = filter.optionsQuery !== undefined
|
|
28
|
-
? (optionsQueryResult.data?.rows ?? [])
|
|
29
|
-
: (filter.options ?? []).map((o) => ({ value: o.value, label: t(o.label) }));
|
|
30
|
-
const allLabel = t(filter.allLabel ?? "kumiko.dashboard.filter.all");
|
|
31
|
-
const options = [{ value: "", label: allLabel }, ...dynamicOptions];
|
|
32
|
-
const picker = (_jsx("div", { className: "max-w-xs", children: _jsx(Field, { id: `dashboard-filter-${filter.id}`, label: t(filter.label), children: _jsx(Input, { kind: "combobox", id: `dashboard-filter-${filter.id}`, name: filter.id, options: options, value: value, onChange: (next) => nav.setSearchParams({ [filter.id]: next === "" ? null : next }), placeholder: filter.placeholder !== undefined ? t(filter.placeholder) : allLabel }) }) }));
|
|
33
|
-
const params = value === "" ? {} : { [filter.id]: value };
|
|
34
|
-
return { params, picker };
|
|
35
|
-
}
|
|
36
|
-
// An unknown/stale URL value falls back to the declared default.
|
|
37
|
-
function useTimeRange(screen, translate) {
|
|
38
|
-
const t = useTranslation();
|
|
39
|
-
const nav = useNav();
|
|
40
|
-
const resolver = useLocale();
|
|
41
|
-
const timeRange = screen.timeRange;
|
|
42
|
-
if (timeRange === undefined)
|
|
43
|
-
return { params: {}, control: null };
|
|
44
|
-
const fromUrl = nav.searchParams[timeRange.id];
|
|
45
|
-
const value = timeRange.options.find((o) => o.value === fromUrl)?.value ?? timeRange.default;
|
|
46
|
-
return {
|
|
47
|
-
// Metric buckets are cut server-side; without the user's zone they default
|
|
48
|
-
// to UTC while the axis labels are formatted in the user's zone.
|
|
49
|
-
params: { [timeRange.id]: value, timeZone: resolver.timeZone() },
|
|
50
|
-
control: (_jsx(ModeSwitch, { value: value, options: timeRange.options.map((o) => ({ value: o.value, label: translate(o.label) })), onChange: (next) => nav.setSearchParams({ [timeRange.id]: next }), ariaLabel: t("kumiko.dashboard.time-range"), testId: `dashboard-time-range-${timeRange.id}` })),
|
|
51
|
-
};
|
|
52
|
-
}
|
|
6
|
+
import { ScreenPanelTile } from "./dashboard-screen-tiles.js";
|
|
7
|
+
import { DashboardQueryContext, HALF_PANEL, PanelLoadedContext, useDashboardFormats, WIDE_PANEL, } from "./dashboard-shared.js";
|
|
8
|
+
import { GatedGridCell, useDashboardVisibility, VisibilityGateQueries, VisibilityQueryContext, } from "./dashboard-visibility.js";
|
|
9
|
+
import { useDashboardHeaderActions, useDashboardRecord, useDashboardScreenParams, } from "./use-dashboard-screen-state.js";
|
|
53
10
|
function panelSpanClassName(panel) {
|
|
54
11
|
if (panel.kind === "stat")
|
|
55
12
|
return undefined;
|
|
@@ -88,212 +45,51 @@ function PanelBody({ panel, label, screenId, screenParams, translate, }) {
|
|
|
88
45
|
}
|
|
89
46
|
return (_jsx(ProgressListPanelBody, { panel: panel, label: text, screenParams: screenParams, translate: translate }));
|
|
90
47
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return { key: `${query}|${JSON.stringify(payload)}`, query, payload };
|
|
95
|
-
}
|
|
96
|
-
// Embedded screens carry no screen filter; in-grid panels pass the filter
|
|
97
|
-
// (unless ignoreScreenFilter, never for a stat-group) and the time range.
|
|
98
|
-
function panelVisibilityGate(panel, screenParams) {
|
|
99
|
-
if (panel.kind === "custom" || panel.visibleWhen === undefined)
|
|
100
|
-
return undefined;
|
|
101
|
-
if (panel.kind === "screen")
|
|
102
|
-
return visibilityGate(panel.visibleWhen.query, NO_GATE_PAYLOAD);
|
|
103
|
-
const dropsFilter = panel.kind !== "stat-group" && panel.kind !== "text" && panel.ignoreScreenFilter === true;
|
|
104
|
-
return visibilityGate(panel.visibleWhen.query, {
|
|
105
|
-
...(dropsFilter ? {} : screenParams.filterParams),
|
|
106
|
-
...screenParams.rangeParams,
|
|
107
|
-
...screenParams.recordParams,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
// One live query per distinct gate (query + payload), shared by every panel
|
|
111
|
-
// that gates on it — N panels on the same status query would otherwise each
|
|
112
|
-
// open their own request, SSE subscription and refetch per event. The queries
|
|
113
|
-
// run as keyed siblings of the grid so a changing gate count never changes the
|
|
114
|
-
// grid's position in the tree (which would remount every panel).
|
|
115
|
-
function VisibilityGateQuery({ gate, onResult, onForget, }) {
|
|
116
|
-
const { data, error, loading, refetch } = useDashboardQuery(gate.query, gate.payload);
|
|
117
|
-
const { key } = gate;
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
onResult(key, { data, error, loading, refetch });
|
|
120
|
-
}, [key, data, error, loading, refetch, onResult]);
|
|
121
|
-
useEffect(() => () => onForget(key), [key, onForget]);
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
function VisibilityGateQueries({ gates, onResult, onForget, }) {
|
|
125
|
-
return gates.map((gate) => (_jsx(VisibilityGateQuery, { gate: gate, onResult: onResult, onForget: onForget }, gate.key)));
|
|
126
|
-
}
|
|
127
|
-
function useVisibilityGateResults() {
|
|
128
|
-
const [results, setResults] = useState(new Map());
|
|
129
|
-
const report = useCallback((key, result) => {
|
|
130
|
-
setResults((prev) => new Map(prev).set(key, result));
|
|
131
|
-
}, []);
|
|
132
|
-
const forget = useCallback((key) => {
|
|
133
|
-
setResults((prev) => {
|
|
134
|
-
if (!prev.has(key))
|
|
135
|
-
return prev;
|
|
136
|
-
const next = new Map(prev);
|
|
137
|
-
next.delete(key);
|
|
138
|
-
return next;
|
|
139
|
-
});
|
|
140
|
-
}, []);
|
|
141
|
-
return { results, report, forget };
|
|
142
|
-
}
|
|
143
|
-
const NOOP_REFETCH = async () => { };
|
|
144
|
-
// A gate reports through an effect, so for its first commit (and after a payload change) no
|
|
145
|
-
// result exists yet; without a placeholder that reads as "hidden" and flashes the fallback.
|
|
146
|
-
function withPendingGates(results, gateKeys) {
|
|
147
|
-
if (gateKeys.every((key) => results.has(key)))
|
|
148
|
-
return results;
|
|
149
|
-
const merged = new Map(results);
|
|
150
|
-
for (const key of gateKeys) {
|
|
151
|
-
if (!merged.has(key)) {
|
|
152
|
-
merged.set(key, { data: null, error: null, loading: true, refetch: NOOP_REFETCH });
|
|
153
|
-
}
|
|
48
|
+
function renderDashboardPanel(panel, featureName, screenId, screenParams, translate) {
|
|
49
|
+
if (panel.kind === "screen") {
|
|
50
|
+
return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, translate: translate }, panel.id));
|
|
154
51
|
}
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
function distinctVisibilityGates(panels, screenParams) {
|
|
158
|
-
const gates = new Map();
|
|
159
|
-
for (const panel of panels) {
|
|
160
|
-
const gate = panelVisibilityGate(panel, screenParams);
|
|
161
|
-
if (gate !== undefined)
|
|
162
|
-
gates.set(gate.key, gate);
|
|
163
|
-
}
|
|
164
|
-
return [...gates.values()];
|
|
165
|
-
}
|
|
166
|
-
function GatedGridCell({ panel, screenParams, label, className, children, }) {
|
|
167
|
-
const gate = panelVisibilityGate(panel, screenParams);
|
|
168
|
-
const visibility = useContext(VisibilityQueryContext).get(gate?.key ?? "");
|
|
169
|
-
const visibleWhen = panel.kind === "custom" ? undefined : panel.visibleWhen;
|
|
170
|
-
if (visibleWhen !== undefined) {
|
|
171
|
-
const verdict = evalVisibleWhen(visibleWhen, visibility);
|
|
172
|
-
if (verdict === "error" && visibility?.error) {
|
|
173
|
-
return (_jsx("div", { className: cn("min-w-0", className), "data-testid": `dashboard-panel-${panel.id}`, children: _jsx(PanelError, { label: label, error: visibility.error, onRetry: () => void visibility.refetch() }) }));
|
|
174
|
-
}
|
|
175
|
-
// Loading renders nothing too: a skeleton that then vanishes flickers.
|
|
176
|
-
if (verdict !== "visible")
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
return _jsx("div", { className: cn("min-w-0", className), children: children });
|
|
180
|
-
}
|
|
181
|
-
function EmbeddedScreenPanelTile({ panel, target, translate, }) {
|
|
182
|
-
const screenBody = _jsx(KumikoScreen, { schema: target.schema, qn: target.qn, translate: translate });
|
|
183
|
-
const embedded = panel.chromeless === true ? (_jsx(EmbeddedFormProvider, { children: _jsx(EmbeddedScreenProvider, { children: screenBody }) })) : (screenBody);
|
|
184
|
-
return (_jsx("div", { className: cn("min-w-0", WIDE_PANEL), "data-testid": `dashboard-panel-${panel.id}`, children: panel.label !== undefined && panel.chromeless !== true ? (_jsx(SectionCard, { title: translate(panel.label), children: embedded })) : (_jsxs(_Fragment, { children: [panel.label !== undefined && (_jsx("h3", { className: "mb-3 text-sm font-semibold text-foreground", children: translate(panel.label) })), embedded] })) }));
|
|
185
|
-
}
|
|
186
|
-
// Own component so the target lookup (a hook) only runs for panels that actually fall back.
|
|
187
|
-
function FallbackScreenPanelTile({ panel, fallback, featureName, translate, }) {
|
|
188
|
-
const target = useEmbeddedScreen(featureName, fallback);
|
|
189
|
-
if (target === undefined)
|
|
190
|
-
return null;
|
|
191
|
-
return _jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, translate: translate });
|
|
192
|
-
}
|
|
193
|
-
function ScreenPanelTile({ panel, featureName, translate, }) {
|
|
194
|
-
const visibleWhen = panel.visibleWhen;
|
|
195
|
-
const visibility = useContext(VisibilityQueryContext).get(visibleWhen !== undefined ? visibilityGate(visibleWhen.query, NO_GATE_PAYLOAD).key : "");
|
|
196
|
-
const target = useEmbeddedScreen(featureName, panel.screen);
|
|
197
|
-
const verdict = visibleWhen === undefined ? "visible" : evalVisibleWhen(visibleWhen, visibility);
|
|
198
|
-
// Nothing while the gate loads: a fallback that flashes before the real panel flickers.
|
|
199
|
-
if (verdict === "loading")
|
|
200
|
-
return null;
|
|
201
|
-
if (verdict !== "visible" && panel.fallback !== undefined) {
|
|
202
|
-
return (_jsx(FallbackScreenPanelTile, { panel: panel, fallback: panel.fallback, featureName: featureName, translate: translate }));
|
|
203
|
-
}
|
|
204
|
-
if (target === undefined)
|
|
205
|
-
return null;
|
|
206
|
-
// A failed gate query must not silently drop the panel: on the
|
|
207
|
-
// account-security page that would hide every MFA enable/disable path.
|
|
208
|
-
if (verdict === "error" && visibility?.error) {
|
|
209
|
-
return (_jsx("div", { className: cn("min-w-0", WIDE_PANEL), "data-testid": `dashboard-panel-${panel.id}`, children: _jsx(PanelError, { label: panel.label !== undefined ? translate(panel.label) : panel.id, error: visibility.error, onRetry: () => void visibility.refetch() }) }));
|
|
210
|
-
}
|
|
211
|
-
if (verdict !== "visible")
|
|
212
|
-
return null;
|
|
213
|
-
return _jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, translate: translate });
|
|
214
|
-
}
|
|
215
|
-
const NO_RECORD_ROW = {};
|
|
216
|
-
function recordTitleText(row, field) {
|
|
217
|
-
if (row === null || field === undefined)
|
|
218
|
-
return undefined;
|
|
219
|
-
const value = row[field];
|
|
220
|
-
if (typeof value !== "string" && typeof value !== "number")
|
|
221
|
-
return undefined;
|
|
222
|
-
return String(value) === "" ? undefined : String(value);
|
|
52
|
+
const label = panel.kind === "custom" || panel.label === undefined ? undefined : translate(panel.label);
|
|
53
|
+
return (_jsx(GatedGridCell, { panel: panel, screenParams: screenParams, label: label ?? panel.id, className: panelSpanClassName(panel), children: _jsx(PanelBody, { panel: panel, label: label, screenId: screenId, screenParams: screenParams, translate: translate }) }, panel.id));
|
|
223
54
|
}
|
|
224
55
|
export function WebDashboardBody({ featureName, screen, translate, }) {
|
|
225
56
|
const t = useTranslation();
|
|
226
57
|
const effectiveTranslate = translate ?? t;
|
|
227
|
-
const
|
|
58
|
+
const primitives = usePrimitives();
|
|
228
59
|
const nav = useNav();
|
|
229
60
|
const headerSlotAvailable = usePageHeaderSlotAvailable();
|
|
230
61
|
const headerCompactContext = usePageHeaderCompact();
|
|
231
62
|
const { formatDateTime } = useDashboardFormats();
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
const
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const visibilityGates = distinctVisibilityGates(screen.panels, screenParams);
|
|
243
|
-
const gateResults = useVisibilityGateResults();
|
|
244
|
-
const gateKeysSignature = visibilityGates.map((gate) => gate.key).join("\n");
|
|
245
|
-
const gateResultsWithPending = useMemo(() => withPendingGates(gateResults.results, gateKeysSignature === "" ? [] : gateKeysSignature.split("\n")), [gateResults.results, gateKeysSignature]);
|
|
246
|
-
// The record query is not a panel query: it needs the interval before the
|
|
247
|
-
// registry context below exists, and `refresh.field` reads from its own row.
|
|
248
|
-
const [refreshIntervalMs, setRefreshIntervalMs] = useState(() => resolveRefreshIntervalMs(screen.refresh, null));
|
|
249
|
-
const recordQuery = useQuery(recordQueryName ?? "", recordParams, {
|
|
250
|
-
enabled: recordQueryName !== undefined && !recordMissing,
|
|
251
|
-
live: true,
|
|
252
|
-
...(refreshIntervalMs !== undefined && { refetchIntervalMs: refreshIntervalMs }),
|
|
253
|
-
});
|
|
254
|
-
const { data: recordRow, refetch: refetchRecord } = recordQuery;
|
|
255
|
-
useEffect(() => {
|
|
256
|
-
setRefreshIntervalMs(resolveRefreshIntervalMs(screen.refresh, recordRow));
|
|
257
|
-
}, [screen.refresh, recordRow]);
|
|
258
|
-
const { register, refetchRegistered } = useRefetchRegistry();
|
|
259
|
-
const queryRuntime = useMemo(() => ({ refreshIntervalMs, register }), [refreshIntervalMs, register]);
|
|
260
|
-
const refetchAll = useCallback(async () => {
|
|
261
|
-
await Promise.all([
|
|
262
|
-
recordQueryName !== undefined ? refetchRecord() : Promise.resolve(),
|
|
263
|
-
refetchRegistered(),
|
|
264
|
-
]);
|
|
265
|
-
}, [recordQueryName, refetchRecord, refetchRegistered]);
|
|
266
|
-
const [actionError, setActionError] = useState(null);
|
|
267
|
-
const idOnlyRow = useMemo(() => (recordPayloadKey !== undefined ? { id: recordId, [recordPayloadKey]: recordId } : null), [recordId, recordPayloadKey]);
|
|
268
|
-
const actionRow = recordQueryName !== undefined ? recordRow : idOnlyRow;
|
|
269
|
-
const headerActions = useRecordActions({
|
|
270
|
-
actions: actionRow !== null && !recordMissing ? screen.actions : undefined,
|
|
271
|
-
record: actionRow ?? NO_RECORD_ROW,
|
|
272
|
-
recordId,
|
|
273
|
-
translate: effectiveTranslate,
|
|
274
|
-
onWriteSuccess: refetchAll,
|
|
275
|
-
});
|
|
276
|
-
const scope = screen.scope;
|
|
277
|
-
// A raw description is agent-facing prose; only an i18n key is user-facing copy.
|
|
278
|
-
const translatedDescription = screen.description !== undefined ? effectiveTranslate(screen.description) : undefined;
|
|
279
|
-
const description = translatedDescription !== screen.description ? translatedDescription : undefined;
|
|
280
|
-
const scopeBadge = scope !== undefined ? (_jsx(StatusBadge, { tone: "accent", testId: `dashboard-${screen.id}-scope`, children: effectiveTranslate(scope.badge) })) : null;
|
|
281
|
-
if (recordMissing) {
|
|
282
|
-
return (_jsx(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: _jsx(Banner, { variant: "info", testId: `dashboard-${screen.id}-record-missing`, children: t("kumiko.dashboard.record.missing") }) }));
|
|
63
|
+
const params = useDashboardScreenParams(screen, effectiveTranslate, nav);
|
|
64
|
+
const { screenParams } = params;
|
|
65
|
+
const visibility = useDashboardVisibility(screen.panels, screenParams);
|
|
66
|
+
const record = useDashboardRecord(screen, params);
|
|
67
|
+
const { recordQuery, recordRow, refetchRecord } = record;
|
|
68
|
+
const { actionError, setActionError, headerActions } = useDashboardHeaderActions(screen, params, record, effectiveTranslate);
|
|
69
|
+
const description = dashboardDescription(screen, effectiveTranslate);
|
|
70
|
+
const scopeBadge = buildScopeBadge(screen, effectiveTranslate);
|
|
71
|
+
if (params.recordMissing) {
|
|
72
|
+
return (_jsx(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: renderRecordMissing(primitives, t, screen.id) }));
|
|
283
73
|
}
|
|
284
|
-
const usesPageHeaderSlot = headerSlotAvailable && PageHeader !== undefined;
|
|
285
|
-
const
|
|
286
|
-
|
|
74
|
+
const usesPageHeaderSlot = headerSlotAvailable && primitives.PageHeader !== undefined;
|
|
75
|
+
const model = {
|
|
76
|
+
screen,
|
|
77
|
+
t,
|
|
78
|
+
translate: effectiveTranslate,
|
|
79
|
+
primitives,
|
|
80
|
+
headerActions,
|
|
81
|
+
setActionError,
|
|
82
|
+
headerCompactContext,
|
|
83
|
+
usesPageHeaderSlot,
|
|
84
|
+
recordTitle: recordTitleText(recordRow, params.recordBinding?.titleField),
|
|
85
|
+
scopeBadge,
|
|
86
|
+
picker: params.picker,
|
|
87
|
+
rangeControl: params.rangeControl,
|
|
88
|
+
loadedAtMs: params.loadedAtMs,
|
|
89
|
+
formatDateTime,
|
|
90
|
+
};
|
|
91
|
+
const headerActionsBar = renderHeaderActionsBar(model);
|
|
287
92
|
// The shell header owns the title; the badge and the actions sit in it there.
|
|
288
93
|
const badgeInHeader = scopeBadge !== null && usesPageHeaderSlot;
|
|
289
|
-
|
|
290
|
-
return (_jsx(PanelLoadedContext.Provider, { value: reportLoaded, children: _jsx(DashboardQueryContext.Provider, { value: queryRuntime, children: _jsxs(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: [showPageHeader && (_jsx(PageHeader, { ...(recordTitle !== undefined && { title: recordTitle }), ...(badgeInHeader && { status: scopeBadge }), ...(headerActionsBar !== null && { actions: headerActionsBar }) })), _jsxs("div", { className: "flex flex-wrap items-end justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [!usesPageHeaderSlot && recordTitle !== undefined && (_jsx(Heading, { variant: "page", testId: `dashboard-${screen.id}-record-title`, children: recordTitle })), !badgeInHeader && scopeBadge, picker] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [!usesPageHeaderSlot && headerActionsBar, screen.showUpdatedAt !== false && (_jsx("span", { "data-testid": `dashboard-${screen.id}-updated-at`, className: "text-xs tabular-nums text-muted-foreground", children: t("kumiko.dashboard.updated-at", { time: formatDateTime(loadedAtMs) }) })), rangeControl] })] }), actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] })), recordQuery.error !== null && recordRow === null && (_jsxs("div", { role: "alert", "data-testid": `dashboard-${screen.id}-record-error`, className: "flex flex-col items-start gap-2 text-sm", children: [_jsx("span", { className: "text-status-bad", children: dispatcherErrorText(recordQuery.error, t) }), _jsx(Button, { variant: "secondary", onClick: () => void refetchRecord(), children: t("kumiko.dashboard.panel.error.retry") })] })), description !== undefined && (_jsx(Text, { variant: "muted", testId: `dashboard-${screen.id}-description`, children: description })), scope?.notice !== undefined && (_jsx(Banner, { variant: "info", testId: `dashboard-${screen.id}-scope-notice`, children: effectiveTranslate(scope.notice) })), _jsxs(VisibilityQueryContext.Provider, { value: gateResultsWithPending, children: [_jsx(VisibilityGateQueries, { gates: visibilityGates, onResult: gateResults.report, onForget: gateResults.forget }), _jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: screen.panels.map((panel) => {
|
|
291
|
-
if (panel.kind === "screen") {
|
|
292
|
-
return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, translate: effectiveTranslate }, panel.id));
|
|
293
|
-
}
|
|
294
|
-
const label = panel.kind === "custom" || panel.label === undefined
|
|
295
|
-
? undefined
|
|
296
|
-
: effectiveTranslate(panel.label);
|
|
297
|
-
return (_jsx(GatedGridCell, { panel: panel, screenParams: screenParams, label: label ?? panel.id, className: panelSpanClassName(panel), children: _jsx(PanelBody, { panel: panel, label: label, screenId: screen.id, screenParams: screenParams, translate: effectiveTranslate }) }, panel.id));
|
|
298
|
-
}) })] })] }) }) }));
|
|
94
|
+
return (_jsx(PanelLoadedContext.Provider, { value: params.reportLoaded, children: _jsx(DashboardQueryContext.Provider, { value: record.queryRuntime, children: _jsxs(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: [renderPageHeaderSlot(model, headerActionsBar, badgeInHeader), renderTitleRow(model, headerActionsBar, badgeInHeader), renderActionErrorBanner(primitives, actionError), renderRecordError(model, recordQuery.error, recordRow, refetchRecord), renderDescriptionAndNotice(model, description), _jsxs(VisibilityQueryContext.Provider, { value: visibility.gateResultsWithPending, children: [_jsx(VisibilityGateQueries, { gates: visibility.visibilityGates, onResult: visibility.gateResults.report, onForget: visibility.gateResults.forget }), _jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: screen.panels.map((panel) => renderDashboardPanel(panel, featureName, screen.id, screenParams, effectiveTranslate)) })] })] }) }) }));
|
|
299
95
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { DashboardScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { Dispatch, ReactNode, SetStateAction } from "react";
|
|
3
|
+
import { type ErrorNotice } from "../../app/error-reference.tsx";
|
|
4
|
+
import type { DispatcherError, Translate } from "../../headless/index.ts";
|
|
5
|
+
import { type usePrimitives, type useRecordActions } from "../../index.ts";
|
|
6
|
+
import type { useDashboardFormats, useLoadedAt } from "./dashboard-shared.tsx";
|
|
7
|
+
type Primitives = ReturnType<typeof usePrimitives>;
|
|
8
|
+
export type DashboardHeaderModel = {
|
|
9
|
+
readonly screen: DashboardScreenDefinition;
|
|
10
|
+
readonly t: Translate;
|
|
11
|
+
readonly translate: Translate;
|
|
12
|
+
readonly primitives: Primitives;
|
|
13
|
+
readonly headerActions: ReturnType<typeof useRecordActions>;
|
|
14
|
+
readonly setActionError: Dispatch<SetStateAction<ErrorNotice | null>>;
|
|
15
|
+
readonly headerCompactContext: boolean;
|
|
16
|
+
readonly usesPageHeaderSlot: boolean;
|
|
17
|
+
readonly recordTitle: string | undefined;
|
|
18
|
+
readonly scopeBadge: ReactNode;
|
|
19
|
+
readonly picker: ReactNode;
|
|
20
|
+
readonly rangeControl: ReactNode;
|
|
21
|
+
readonly loadedAtMs: ReturnType<typeof useLoadedAt>["loadedAtMs"];
|
|
22
|
+
readonly formatDateTime: ReturnType<typeof useDashboardFormats>["formatDateTime"];
|
|
23
|
+
};
|
|
24
|
+
export declare function recordTitleText(row: Readonly<Record<string, unknown>> | null, field: string | undefined): string | undefined;
|
|
25
|
+
export declare function buildScopeBadge(screen: DashboardScreenDefinition, translate: Translate): ReactNode;
|
|
26
|
+
export declare function dashboardDescription(screen: DashboardScreenDefinition, translate: Translate): string | undefined;
|
|
27
|
+
export declare function renderRecordMissing(primitives: Primitives, t: Translate, screenId: string): ReactNode;
|
|
28
|
+
export declare function renderHeaderActionsBar(model: DashboardHeaderModel): ReactNode;
|
|
29
|
+
export declare function renderPageHeaderSlot(model: DashboardHeaderModel, headerActionsBar: ReactNode, badgeInHeader: boolean): ReactNode;
|
|
30
|
+
export declare function renderTitleRow(model: DashboardHeaderModel, headerActionsBar: ReactNode, badgeInHeader: boolean): ReactNode;
|
|
31
|
+
export declare function renderActionErrorBanner(primitives: Primitives, actionError: ErrorNotice | null): ReactNode;
|
|
32
|
+
export declare function renderRecordError(model: DashboardHeaderModel, error: DispatcherError | null, recordRow: Readonly<Record<string, unknown>> | null, refetchRecord: () => Promise<void>): ReactNode;
|
|
33
|
+
export declare function renderDescriptionAndNotice(model: DashboardHeaderModel, description: string | undefined): ReactNode;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ErrorReference } from "../../app/error-reference.js";
|
|
3
|
+
import { dispatcherErrorText, HeaderActionsBar, } from "../../index.js";
|
|
4
|
+
import { StatusBadge } from "../widgets/status-badge.js";
|
|
5
|
+
export function recordTitleText(row, field) {
|
|
6
|
+
if (row === null || field === undefined)
|
|
7
|
+
return undefined;
|
|
8
|
+
const value = row[field];
|
|
9
|
+
if (typeof value !== "string" && typeof value !== "number")
|
|
10
|
+
return undefined;
|
|
11
|
+
return String(value) === "" ? undefined : String(value);
|
|
12
|
+
}
|
|
13
|
+
export function buildScopeBadge(screen, translate) {
|
|
14
|
+
const scope = screen.scope;
|
|
15
|
+
return scope !== undefined ? (_jsx(StatusBadge, { tone: "accent", testId: `dashboard-${screen.id}-scope`, children: translate(scope.badge) })) : null;
|
|
16
|
+
}
|
|
17
|
+
export function dashboardDescription(screen, translate) {
|
|
18
|
+
// A raw description is agent-facing prose; only an i18n key is user-facing copy.
|
|
19
|
+
const translatedDescription = screen.description !== undefined ? translate(screen.description) : undefined;
|
|
20
|
+
return translatedDescription !== screen.description ? translatedDescription : undefined;
|
|
21
|
+
}
|
|
22
|
+
export function renderRecordMissing(primitives, t, screenId) {
|
|
23
|
+
const { Banner } = primitives;
|
|
24
|
+
return (_jsx(Banner, { variant: "info", testId: `dashboard-${screenId}-record-missing`, children: t("kumiko.dashboard.record.missing") }));
|
|
25
|
+
}
|
|
26
|
+
export function renderHeaderActionsBar(model) {
|
|
27
|
+
const { screen, headerActions, headerCompactContext, usesPageHeaderSlot, setActionError } = model;
|
|
28
|
+
const { Button, Dialog, ActionOverflowMenu, Grid, PageHeader } = model.primitives;
|
|
29
|
+
return headerActions !== undefined && headerActions.length > 0 ? (_jsx(Grid, { columns: "auto", testId: `dashboard-${screen.id}-actions`, children: _jsx(HeaderActionsBar, { actions: headerActions, Button: Button, Dialog: Dialog, ActionOverflowMenu: ActionOverflowMenu, onError: setActionError, collapseAfterPrimary: usesPageHeaderSlot, compact: headerCompactContext && usesPageHeaderSlot, PageHeader: PageHeader, overflowTestId: `dashboard-${screen.id}-actions-overflow` }) })) : null;
|
|
30
|
+
}
|
|
31
|
+
export function renderPageHeaderSlot(model, headerActionsBar, badgeInHeader) {
|
|
32
|
+
const { PageHeader } = model.primitives;
|
|
33
|
+
const { recordTitle, scopeBadge, usesPageHeaderSlot } = model;
|
|
34
|
+
const showPageHeader = usesPageHeaderSlot && (badgeInHeader || recordTitle !== undefined || headerActionsBar !== null);
|
|
35
|
+
return (showPageHeader &&
|
|
36
|
+
PageHeader !== undefined && (_jsx(PageHeader, { ...(recordTitle !== undefined && { title: recordTitle }), ...(badgeInHeader && { status: scopeBadge }), ...(headerActionsBar !== null && { actions: headerActionsBar }) })));
|
|
37
|
+
}
|
|
38
|
+
export function renderTitleRow(model, headerActionsBar, badgeInHeader) {
|
|
39
|
+
const { Heading } = model.primitives;
|
|
40
|
+
const { screen, usesPageHeaderSlot, recordTitle, scopeBadge, picker, rangeControl } = model;
|
|
41
|
+
return (_jsxs("div", { className: "flex flex-wrap items-end justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [!usesPageHeaderSlot && recordTitle !== undefined && (_jsx(Heading, { variant: "page", testId: `dashboard-${screen.id}-record-title`, children: recordTitle })), !badgeInHeader && scopeBadge, picker] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [!usesPageHeaderSlot && headerActionsBar, screen.showUpdatedAt !== false && (_jsx("span", { "data-testid": `dashboard-${screen.id}-updated-at`, className: "text-xs tabular-nums text-muted-foreground", children: model.t("kumiko.dashboard.updated-at", {
|
|
42
|
+
time: model.formatDateTime(model.loadedAtMs),
|
|
43
|
+
}) })), rangeControl] })] }));
|
|
44
|
+
}
|
|
45
|
+
export function renderActionErrorBanner(primitives, actionError) {
|
|
46
|
+
const { Banner } = primitives;
|
|
47
|
+
return (actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] })));
|
|
48
|
+
}
|
|
49
|
+
export function renderRecordError(model, error, recordRow, refetchRecord) {
|
|
50
|
+
const { Button } = model.primitives;
|
|
51
|
+
return (error !== null &&
|
|
52
|
+
recordRow === null && (_jsxs("div", { role: "alert", "data-testid": `dashboard-${model.screen.id}-record-error`, className: "flex flex-col items-start gap-2 text-sm", children: [_jsx("span", { className: "text-status-bad", children: dispatcherErrorText(error, model.t) }), _jsx(Button, { variant: "secondary", onClick: () => void refetchRecord(), children: model.t("kumiko.dashboard.panel.error.retry") })] })));
|
|
53
|
+
}
|
|
54
|
+
export function renderDescriptionAndNotice(model, description) {
|
|
55
|
+
const { Text, Banner } = model.primitives;
|
|
56
|
+
const { screen } = model;
|
|
57
|
+
return (_jsxs(_Fragment, { children: [description !== undefined && (_jsx(Text, { variant: "muted", testId: `dashboard-${screen.id}-description`, children: description })), screen.scope?.notice !== undefined && (_jsx(Banner, { variant: "info", testId: `dashboard-${screen.id}-scope-notice`, children: model.translate(screen.scope.notice) }))] }));
|
|
58
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DashboardScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { Translate } from "../../headless/index.ts";
|
|
4
|
+
export declare function useFilterParams(screen: DashboardScreenDefinition, enabled: boolean): {
|
|
5
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
6
|
+
readonly picker: ReactNode;
|
|
7
|
+
};
|
|
8
|
+
export declare function useTimeRange(screen: DashboardScreenDefinition, translate: Translate): {
|
|
9
|
+
readonly params: Readonly<Record<string, unknown>>;
|
|
10
|
+
readonly control: ReactNode;
|
|
11
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useLocale, useNav, usePrimitives, useQuery, useTranslation } from "../../index.js";
|
|
3
|
+
import { ModeSwitch } from "../widgets/mode-switch.js";
|
|
4
|
+
// Value lives in the URL under `filter.id` (like useListUrlState's
|
|
5
|
+
// `<screenId>.page`, same replaceState) so a `navigate` rowAction with
|
|
6
|
+
// `params` can deep-link onto this filter.
|
|
7
|
+
export function useFilterParams(screen, enabled) {
|
|
8
|
+
const { Field, Input } = usePrimitives();
|
|
9
|
+
const t = useTranslation();
|
|
10
|
+
const nav = useNav();
|
|
11
|
+
const filter = screen.filter;
|
|
12
|
+
const value = filter !== undefined ? (nav.searchParams[filter.id] ?? "") : "";
|
|
13
|
+
const optionsQueryResult = useQuery(filter?.optionsQuery ?? "", {}, { enabled: enabled && filter?.optionsQuery !== undefined });
|
|
14
|
+
if (filter === undefined) {
|
|
15
|
+
return { params: {}, picker: null };
|
|
16
|
+
}
|
|
17
|
+
const dynamicOptions = filter.optionsQuery !== undefined
|
|
18
|
+
? (optionsQueryResult.data?.rows ?? [])
|
|
19
|
+
: (filter.options ?? []).map((o) => ({ value: o.value, label: t(o.label) }));
|
|
20
|
+
const allLabel = t(filter.allLabel ?? "kumiko.dashboard.filter.all");
|
|
21
|
+
const options = [{ value: "", label: allLabel }, ...dynamicOptions];
|
|
22
|
+
const picker = (_jsx("div", { className: "max-w-xs", children: _jsx(Field, { id: `dashboard-filter-${filter.id}`, label: t(filter.label), children: _jsx(Input, { kind: "combobox", id: `dashboard-filter-${filter.id}`, name: filter.id, options: options, value: value, onChange: (next) => nav.setSearchParams({ [filter.id]: next === "" ? null : next }), placeholder: filter.placeholder !== undefined ? t(filter.placeholder) : allLabel }) }) }));
|
|
23
|
+
const params = value === "" ? {} : { [filter.id]: value };
|
|
24
|
+
return { params, picker };
|
|
25
|
+
}
|
|
26
|
+
// An unknown/stale URL value falls back to the declared default.
|
|
27
|
+
export function useTimeRange(screen, translate) {
|
|
28
|
+
const t = useTranslation();
|
|
29
|
+
const nav = useNav();
|
|
30
|
+
const resolver = useLocale();
|
|
31
|
+
const timeRange = screen.timeRange;
|
|
32
|
+
if (timeRange === undefined)
|
|
33
|
+
return { params: {}, control: null };
|
|
34
|
+
const fromUrl = nav.searchParams[timeRange.id];
|
|
35
|
+
const value = timeRange.options.find((o) => o.value === fromUrl)?.value ?? timeRange.default;
|
|
36
|
+
return {
|
|
37
|
+
// Metric buckets are cut server-side; without the user's zone they default
|
|
38
|
+
// to UTC while the axis labels are formatted in the user's zone.
|
|
39
|
+
params: { [timeRange.id]: value, timeZone: resolver.timeZone() },
|
|
40
|
+
control: (_jsx(ModeSwitch, { value: value, options: timeRange.options.map((o) => ({ value: o.value, label: translate(o.label) })), onChange: (next) => nav.setSearchParams({ [timeRange.id]: next }), ariaLabel: t("kumiko.dashboard.time-range"), testId: `dashboard-time-range-${timeRange.id}` })),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DashboardScreenPanel } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
import type { Translate } from "../../headless/index.ts";
|
|
4
|
+
export declare function ScreenPanelTile({ panel, featureName, translate, }: {
|
|
5
|
+
readonly panel: DashboardScreenPanel;
|
|
6
|
+
readonly featureName: string;
|
|
7
|
+
readonly translate: Translate;
|
|
8
|
+
}): ReactNode;
|