@tai42/studio-sdk 25.0.1 → 26.0.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/README.md +143 -26
- package/dist/components/action-menu.d.ts +30 -0
- package/dist/components/action-menu.d.ts.map +1 -0
- package/dist/components/action-menu.js +101 -0
- package/dist/components/action-menu.js.map +1 -0
- package/dist/components/agent-picker.d.ts +22 -0
- package/dist/components/agent-picker.d.ts.map +1 -0
- package/dist/components/agent-picker.js +17 -0
- package/dist/components/agent-picker.js.map +1 -0
- package/dist/components/channel-picker.d.ts +27 -0
- package/dist/components/channel-picker.d.ts.map +1 -0
- package/dist/components/channel-picker.js +26 -0
- package/dist/components/channel-picker.js.map +1 -0
- package/dist/components/components.css +226 -8
- package/dist/components/confirm-dialog.d.ts +3 -1
- package/dist/components/confirm-dialog.d.ts.map +1 -1
- package/dist/components/confirm-dialog.js +2 -2
- package/dist/components/confirm-dialog.js.map +1 -1
- package/dist/components/execution-key-picker.d.ts +26 -10
- package/dist/components/execution-key-picker.d.ts.map +1 -1
- package/dist/components/execution-key-picker.js +134 -15
- package/dist/components/execution-key-picker.js.map +1 -1
- package/dist/components/extension-combo-builder.d.ts +7 -7
- package/dist/components/extension-combo-builder.d.ts.map +1 -1
- package/dist/components/extension-combo-builder.js +42 -16
- package/dist/components/extension-combo-builder.js.map +1 -1
- package/dist/components/extension-combo-parts.d.ts +13 -11
- package/dist/components/extension-combo-parts.d.ts.map +1 -1
- package/dist/components/extension-combo-parts.js +10 -19
- package/dist/components/extension-combo-parts.js.map +1 -1
- package/dist/components/extension-config-editor.d.ts +33 -0
- package/dist/components/extension-config-editor.d.ts.map +1 -0
- package/dist/components/extension-config-editor.js +190 -0
- package/dist/components/extension-config-editor.js.map +1 -0
- package/dist/components/extension-grouping.d.ts +4 -41
- package/dist/components/extension-grouping.d.ts.map +1 -1
- package/dist/components/extension-grouping.js +1 -48
- package/dist/components/extension-grouping.js.map +1 -1
- package/dist/components/extension-picker.d.ts +4 -3
- package/dist/components/extension-picker.d.ts.map +1 -1
- package/dist/components/extension-picker.js +6 -6
- package/dist/components/extension-picker.js.map +1 -1
- package/dist/components/fleet-report.d.ts +8 -3
- package/dist/components/fleet-report.d.ts.map +1 -1
- package/dist/components/fleet-report.js +12 -2
- package/dist/components/fleet-report.js.map +1 -1
- package/dist/components/holder-kinds.d.ts +18 -0
- package/dist/components/holder-kinds.d.ts.map +1 -0
- package/dist/components/holder-kinds.js +41 -0
- package/dist/components/holder-kinds.js.map +1 -0
- package/dist/components/holder-list.d.ts +58 -0
- package/dist/components/holder-list.d.ts.map +1 -0
- package/dist/components/holder-list.js +77 -0
- package/dist/components/holder-list.js.map +1 -0
- package/dist/components/icons/index.d.ts +1 -1
- package/dist/components/icons/index.d.ts.map +1 -1
- package/dist/components/icons/index.js +1 -1
- package/dist/components/icons/index.js.map +1 -1
- package/dist/components/icons/nav-map.d.ts.map +1 -1
- package/dist/components/icons/nav-map.js +1 -3
- package/dist/components/icons/nav-map.js.map +1 -1
- package/dist/components/icons/nav-marks.d.ts +0 -4
- package/dist/components/icons/nav-marks.d.ts.map +1 -1
- package/dist/components/icons/nav-marks.js +0 -4
- package/dist/components/icons/nav-marks.js.map +1 -1
- package/dist/components/named-list-picker.d.ts +39 -0
- package/dist/components/named-list-picker.d.ts.map +1 -0
- package/dist/components/named-list-picker.js +61 -0
- package/dist/components/named-list-picker.js.map +1 -0
- package/dist/components/named-list-test-utils.d.ts +23 -0
- package/dist/components/named-list-test-utils.d.ts.map +1 -0
- package/dist/components/named-list-test-utils.js +33 -0
- package/dist/components/named-list-test-utils.js.map +1 -0
- package/dist/components/recycle-roll.d.ts +23 -0
- package/dist/components/recycle-roll.d.ts.map +1 -0
- package/dist/components/recycle-roll.js +47 -0
- package/dist/components/recycle-roll.js.map +1 -0
- package/dist/components/stepper.d.ts +28 -0
- package/dist/components/stepper.d.ts.map +1 -0
- package/dist/components/stepper.js +33 -0
- package/dist/components/stepper.js.map +1 -0
- package/dist/components/subject-section.d.ts +2 -1
- package/dist/components/subject-section.d.ts.map +1 -1
- package/dist/components/subject-section.js +10 -5
- package/dist/components/subject-section.js.map +1 -1
- package/dist/components/tokens.css +31 -41
- package/dist/components/tokens.d.ts +3 -4
- package/dist/components/tokens.d.ts.map +1 -1
- package/dist/components/tokens.js +5 -13
- package/dist/components/tokens.js.map +1 -1
- package/dist/components/tool-picker-parts.d.ts +5 -1
- package/dist/components/tool-picker-parts.d.ts.map +1 -1
- package/dist/components/tool-picker-parts.js +8 -6
- package/dist/components/tool-picker-parts.js.map +1 -1
- package/dist/components/tool-picker.d.ts +77 -2
- package/dist/components/tool-picker.d.ts.map +1 -1
- package/dist/components/tool-picker.js +87 -29
- package/dist/components/tool-picker.js.map +1 -1
- package/dist/components/version-history-panel.d.ts +7 -1
- package/dist/components/version-history-panel.d.ts.map +1 -1
- package/dist/components/version-history-panel.js +2 -2
- package/dist/components/version-history-panel.js.map +1 -1
- package/dist/components/version-history-rows.d.ts +4 -2
- package/dist/components/version-history-rows.d.ts.map +1 -1
- package/dist/components/version-history-rows.js +14 -2
- package/dist/components/version-history-rows.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -1
- package/dist/hooks/useApi.d.ts +524 -197
- package/dist/hooks/useApi.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.d.ts +43 -10
- package/dist/hooks/useCapabilities.d.ts.map +1 -1
- package/dist/hooks/useCapabilities.js +39 -10
- package/dist/hooks/useCapabilities.js.map +1 -1
- package/dist/index.d.ts +25 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -6
- package/dist/index.js.map +1 -1
- package/dist/marketplace-install/env-var-fields.d.ts +17 -0
- package/dist/marketplace-install/env-var-fields.d.ts.map +1 -0
- package/dist/marketplace-install/env-var-fields.js +22 -0
- package/dist/marketplace-install/env-var-fields.js.map +1 -0
- package/dist/marketplace-install/index.d.ts +18 -0
- package/dist/marketplace-install/index.d.ts.map +1 -0
- package/dist/marketplace-install/index.js +8 -0
- package/dist/marketplace-install/index.js.map +1 -0
- package/dist/marketplace-install/install-env.d.ts +21 -0
- package/dist/marketplace-install/install-env.d.ts.map +1 -0
- package/dist/marketplace-install/install-env.js +22 -0
- package/dist/marketplace-install/install-env.js.map +1 -0
- package/dist/marketplace-install/install-mount.d.ts +42 -0
- package/dist/marketplace-install/install-mount.d.ts.map +1 -0
- package/dist/marketplace-install/install-mount.js +62 -0
- package/dist/marketplace-install/install-mount.js.map +1 -0
- package/dist/marketplace-install/install-report.d.ts +14 -0
- package/dist/marketplace-install/install-report.d.ts.map +1 -0
- package/dist/marketplace-install/install-report.js +19 -0
- package/dist/marketplace-install/install-report.js.map +1 -0
- package/dist/marketplace-install/install-shell.d.ts +34 -0
- package/dist/marketplace-install/install-shell.d.ts.map +1 -0
- package/dist/marketplace-install/install-shell.js +32 -0
- package/dist/marketplace-install/install-shell.js.map +1 -0
- package/dist/marketplace-install/mount-install.d.ts +41 -0
- package/dist/marketplace-install/mount-install.d.ts.map +1 -0
- package/dist/marketplace-install/mount-install.js +90 -0
- package/dist/marketplace-install/mount-install.js.map +1 -0
- package/dist/marketplace-install/plugin-detail.d.ts +17 -0
- package/dist/marketplace-install/plugin-detail.d.ts.map +1 -0
- package/dist/marketplace-install/plugin-detail.js +41 -0
- package/dist/marketplace-install/plugin-detail.js.map +1 -0
- package/dist/marketplace-install/plugin-install.d.ts +27 -0
- package/dist/marketplace-install/plugin-install.d.ts.map +1 -0
- package/dist/marketplace-install/plugin-install.js +89 -0
- package/dist/marketplace-install/plugin-install.js.map +1 -0
- package/dist/marketplace-install/route-items.d.ts +14 -0
- package/dist/marketplace-install/route-items.d.ts.map +1 -0
- package/dist/marketplace-install/route-items.js +11 -0
- package/dist/marketplace-install/route-items.js.map +1 -0
- package/dist/marketplace-install/use-debounced-value.d.ts +6 -0
- package/dist/marketplace-install/use-debounced-value.d.ts.map +1 -0
- package/dist/marketplace-install/use-debounced-value.js +18 -0
- package/dist/marketplace-install/use-debounced-value.js.map +1 -0
- package/dist/marketplace-install/use-install-action.d.ts +26 -0
- package/dist/marketplace-install/use-install-action.d.ts.map +1 -0
- package/dist/marketplace-install/use-install-action.js +35 -0
- package/dist/marketplace-install/use-install-action.js.map +1 -0
- package/dist/marketplace-install/use-install-preview.d.ts +33 -0
- package/dist/marketplace-install/use-install-preview.d.ts.map +1 -0
- package/dist/marketplace-install/use-install-preview.js +44 -0
- package/dist/marketplace-install/use-install-preview.js.map +1 -0
- package/dist/marketplace-install/use-mount-install.d.ts +80 -0
- package/dist/marketplace-install/use-mount-install.d.ts.map +1 -0
- package/dist/marketplace-install/use-mount-install.js +75 -0
- package/dist/marketplace-install/use-mount-install.js.map +1 -0
- package/dist/navigation/index.d.ts +1 -1
- package/dist/navigation/index.d.ts.map +1 -1
- package/dist/navigation/index.js.map +1 -1
- package/dist/navigation/types.d.ts +23 -7
- package/dist/navigation/types.d.ts.map +1 -1
- package/dist/plugin/registry.d.ts +0 -8
- package/dist/plugin/registry.d.ts.map +1 -1
- package/dist/plugin/registry.js +25 -1
- package/dist/plugin/registry.js.map +1 -1
- package/dist/plugin/types.d.ts +36 -12
- package/dist/plugin/types.d.ts.map +1 -1
- package/dist/plugin/version.d.ts +6 -4
- package/dist/plugin/version.d.ts.map +1 -1
- package/dist/plugin/version.js +6 -4
- package/dist/plugin/version.js.map +1 -1
- package/dist/query-keys.d.ts +45 -4
- package/dist/query-keys.d.ts.map +1 -1
- package/dist/query-keys.js +52 -4
- package/dist/query-keys.js.map +1 -1
- package/dist/schema-form/json-schema-meta.d.ts +3 -0
- package/dist/schema-form/json-schema-meta.d.ts.map +1 -0
- package/dist/schema-form/json-schema-meta.js +3 -0
- package/dist/schema-form/json-schema-meta.js.map +1 -0
- package/dist/testing.d.ts +2 -0
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -1
- package/dist/token-rules.d.ts +26 -0
- package/dist/token-rules.d.ts.map +1 -0
- package/dist/token-rules.js +64 -0
- package/dist/token-rules.js.map +1 -0
- package/package.json +2 -2
- package/src/components/action-menu.tsx +160 -0
- package/src/components/agent-picker.tsx +53 -0
- package/src/components/channel-picker.tsx +68 -0
- package/src/components/components.css +226 -8
- package/src/components/confirm-dialog.tsx +4 -1
- package/src/components/execution-key-picker.tsx +252 -36
- package/src/components/extension-combo-builder.tsx +61 -29
- package/src/components/extension-combo-parts.tsx +28 -38
- package/src/components/extension-config-editor.tsx +307 -0
- package/src/components/extension-grouping.ts +4 -73
- package/src/components/extension-picker.tsx +13 -13
- package/src/components/fleet-report.tsx +38 -4
- package/src/components/holder-kinds.ts +60 -0
- package/src/components/holder-list.tsx +163 -0
- package/src/components/icons/index.ts +0 -2
- package/src/components/icons/nav-map.ts +0 -4
- package/src/components/icons/nav-marks.tsx +0 -15
- package/src/components/named-list-picker.tsx +182 -0
- package/src/components/named-list-test-utils.tsx +47 -0
- package/src/components/recycle-roll.tsx +120 -0
- package/src/components/stepper.tsx +111 -0
- package/src/components/subject-section.tsx +10 -5
- package/src/components/tokens.css +31 -41
- package/src/components/tokens.ts +5 -13
- package/src/components/tool-picker-parts.tsx +17 -6
- package/src/components/tool-picker.tsx +215 -41
- package/src/components/version-history-panel.tsx +8 -0
- package/src/components/version-history-rows.tsx +14 -2
- package/src/errors.ts +10 -0
- package/src/hooks/useCapabilities.ts +59 -10
- package/src/index.ts +54 -14
- package/src/marketplace-install/env-var-fields.tsx +59 -0
- package/src/marketplace-install/index.ts +17 -0
- package/src/marketplace-install/install-env.ts +39 -0
- package/src/marketplace-install/install-mount.ts +96 -0
- package/src/marketplace-install/install-report.tsx +54 -0
- package/src/marketplace-install/install-shell.tsx +99 -0
- package/src/marketplace-install/mount-install.tsx +290 -0
- package/src/marketplace-install/plugin-detail.ts +60 -0
- package/src/marketplace-install/plugin-install.tsx +221 -0
- package/src/marketplace-install/route-items.ts +20 -0
- package/src/marketplace-install/use-debounced-value.ts +18 -0
- package/src/marketplace-install/use-install-action.ts +60 -0
- package/src/marketplace-install/use-install-preview.ts +74 -0
- package/src/marketplace-install/use-mount-install.ts +116 -0
- package/src/navigation/index.ts +1 -0
- package/src/navigation/types.ts +27 -7
- package/src/plugin/registry.ts +19 -1
- package/src/plugin/types.ts +40 -12
- package/src/plugin/version.ts +6 -4
- package/src/query-keys.ts +71 -4
- package/src/schema-form/json-schema-meta.ts +2 -0
- package/src/testing.ts +2 -0
- package/src/token-rules.ts +66 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `HolderList` — the things that still use a name, each a link to where it lives.
|
|
3
|
+
* A core holder kind links to its Studio screen; a plugin kind links to the page its
|
|
4
|
+
* plugin registered for it; a kind nobody resolves shows as text, so no holder is
|
|
5
|
+
* hidden.
|
|
6
|
+
*/
|
|
7
|
+
import type { ReferenceHolder } from '@tai42/api-client';
|
|
8
|
+
import { type MouseEvent, type ReactNode, useCallback } from 'react';
|
|
9
|
+
|
|
10
|
+
import { errorMessage } from '../errors';
|
|
11
|
+
import { useNavigation } from '../navigation/context';
|
|
12
|
+
import { coreHolderRoute } from './holder-kinds';
|
|
13
|
+
import { Spinner } from './primitives';
|
|
14
|
+
import { ScrollRegion } from './scroll-region';
|
|
15
|
+
|
|
16
|
+
/** Where one holder links: its `href` and the client-side transition to it. */
|
|
17
|
+
export interface HolderHref {
|
|
18
|
+
readonly href: string;
|
|
19
|
+
readonly navigate: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The resolver for holder links: `null` for a kind neither the core table nor any
|
|
24
|
+
* registered plugin resolves.
|
|
25
|
+
*/
|
|
26
|
+
export function useHolderHref(): (holder: ReferenceHolder) => HolderHref | null {
|
|
27
|
+
const { navigate, resolvePath, navigatePlugin, resolvePluginPath, resolveHolderLink } =
|
|
28
|
+
useNavigation();
|
|
29
|
+
return useCallback(
|
|
30
|
+
(holder: ReferenceHolder): HolderHref | null => {
|
|
31
|
+
const core = coreHolderRoute(holder);
|
|
32
|
+
if (core !== null) {
|
|
33
|
+
return {
|
|
34
|
+
href: resolvePath(core.token, core.search),
|
|
35
|
+
navigate: () => {
|
|
36
|
+
navigate(core.token, core.search);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const target = resolveHolderLink?.(holder) ?? null;
|
|
41
|
+
if (target === null) return null;
|
|
42
|
+
const { pluginId, pagePath, params, search } = target;
|
|
43
|
+
return {
|
|
44
|
+
href: resolvePluginPath(pluginId, pagePath, params, search),
|
|
45
|
+
navigate: () => {
|
|
46
|
+
navigatePlugin(pluginId, pagePath, params, search);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
[navigate, resolvePath, navigatePlugin, resolvePluginPath, resolveHolderLink],
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface HolderListProps {
|
|
55
|
+
readonly holders: readonly ReferenceHolder[];
|
|
56
|
+
/** The list's accessible name, e.g. `Holders of <name>`. */
|
|
57
|
+
readonly label: string;
|
|
58
|
+
/** The line above the list saying what to do about the holders. */
|
|
59
|
+
readonly lead: string;
|
|
60
|
+
/** Called before a holder link navigates (a dialog closes itself here). */
|
|
61
|
+
readonly onNavigate?: () => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function HolderLink({
|
|
65
|
+
link,
|
|
66
|
+
onNavigate,
|
|
67
|
+
children,
|
|
68
|
+
}: {
|
|
69
|
+
link: HolderHref;
|
|
70
|
+
onNavigate: (() => void) | undefined;
|
|
71
|
+
children: ReactNode;
|
|
72
|
+
}): ReactNode {
|
|
73
|
+
const onClick = useCallback(
|
|
74
|
+
(event: MouseEvent<HTMLAnchorElement>) => {
|
|
75
|
+
if (event.defaultPrevented) return;
|
|
76
|
+
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
77
|
+
return;
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
onNavigate?.();
|
|
80
|
+
link.navigate();
|
|
81
|
+
},
|
|
82
|
+
[link, onNavigate],
|
|
83
|
+
);
|
|
84
|
+
return (
|
|
85
|
+
<a className="tai-link" href={link.href} onClick={onClick}>
|
|
86
|
+
{children}
|
|
87
|
+
</a>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The lead line, then one list item per holder: a link where one resolves, else text. */
|
|
92
|
+
export function HolderList({ holders, label, lead, onNavigate }: HolderListProps): ReactNode {
|
|
93
|
+
const hrefOf = useHolderHref();
|
|
94
|
+
return (
|
|
95
|
+
<div>
|
|
96
|
+
<p className="tai-holder-lead">{lead}</p>
|
|
97
|
+
<ScrollRegion label={label} axis="vertical" className="tai-holder-list">
|
|
98
|
+
<ul aria-label={label}>
|
|
99
|
+
{holders.map((holder, index) => {
|
|
100
|
+
const link = hrefOf(holder);
|
|
101
|
+
return (
|
|
102
|
+
<li key={`${holder.kind}\u0000${holder.name}\u0000${String(index)}`}>
|
|
103
|
+
{link === null ? (
|
|
104
|
+
holder.summary
|
|
105
|
+
) : (
|
|
106
|
+
<HolderLink link={link} onNavigate={onNavigate}>
|
|
107
|
+
{holder.summary}
|
|
108
|
+
</HolderLink>
|
|
109
|
+
)}
|
|
110
|
+
</li>
|
|
111
|
+
);
|
|
112
|
+
})}
|
|
113
|
+
</ul>
|
|
114
|
+
</ScrollRegion>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* What a dialog knows about the holders of a name before it acts: still reading, the
|
|
121
|
+
* caller has no grant for the read, the read failed, or the holders it read (or the
|
|
122
|
+
* holders a refusal named).
|
|
123
|
+
*/
|
|
124
|
+
export type HolderPreviewState =
|
|
125
|
+
| { readonly status: 'loading' }
|
|
126
|
+
| { readonly status: 'no-grant'; readonly message: string }
|
|
127
|
+
| { readonly status: 'error'; readonly error: unknown }
|
|
128
|
+
| { readonly status: 'ready'; readonly holders: readonly ReferenceHolder[] };
|
|
129
|
+
|
|
130
|
+
export interface HolderPreviewProps extends Omit<HolderListProps, 'holders'> {
|
|
131
|
+
readonly state: HolderPreviewState;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The holder region of a rename, delete or detach dialog. The preview is advisory:
|
|
136
|
+
* loading, no grant and a failed read leave the dialog's action enabled, and the
|
|
137
|
+
* server's refusal decides. An empty preview renders nothing.
|
|
138
|
+
*/
|
|
139
|
+
export function HolderPreview({ state, ...list }: HolderPreviewProps): ReactNode {
|
|
140
|
+
switch (state.status) {
|
|
141
|
+
case 'loading':
|
|
142
|
+
return <Spinner label="Checking what still uses this" />;
|
|
143
|
+
case 'no-grant':
|
|
144
|
+
return (
|
|
145
|
+
<p role="status" className="tai-muted" style={{ margin: 0 }}>
|
|
146
|
+
{state.message}
|
|
147
|
+
</p>
|
|
148
|
+
);
|
|
149
|
+
case 'error':
|
|
150
|
+
return (
|
|
151
|
+
<p role="status" className="tai-muted" style={{ margin: 0 }}>
|
|
152
|
+
Could not check what uses this: {errorMessage(state.error)}
|
|
153
|
+
</p>
|
|
154
|
+
);
|
|
155
|
+
case 'ready':
|
|
156
|
+
return state.holders.length === 0 ? null : <HolderList holders={state.holders} {...list} />;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** The holders a preview state blocks on: a non-empty list disables the action. */
|
|
161
|
+
export function previewHolders(state: HolderPreviewState): readonly ReferenceHolder[] {
|
|
162
|
+
return state.status === 'ready' ? state.holders : [];
|
|
163
|
+
}
|
|
@@ -64,7 +64,6 @@ export {
|
|
|
64
64
|
ConversationsIcon,
|
|
65
65
|
DashboardIcon,
|
|
66
66
|
DatabaseIcon,
|
|
67
|
-
ExtensionsIcon,
|
|
68
67
|
HooksIcon,
|
|
69
68
|
InteractionsIcon,
|
|
70
69
|
ManifestIcon,
|
|
@@ -78,7 +77,6 @@ export {
|
|
|
78
77
|
SettingsIcon,
|
|
79
78
|
StorageIcon,
|
|
80
79
|
SystemIcon,
|
|
81
|
-
TemplatesIcon,
|
|
82
80
|
ToolsIcon,
|
|
83
81
|
} from './nav-marks';
|
|
84
82
|
export { AlertTriangleIcon, CheckCircleIcon, PendingIcon, XCircleIcon } from './status-marks';
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
ConversationsIcon,
|
|
12
12
|
DashboardIcon,
|
|
13
13
|
DatabaseIcon,
|
|
14
|
-
ExtensionsIcon,
|
|
15
14
|
HooksIcon,
|
|
16
15
|
InteractionsIcon,
|
|
17
16
|
ManifestIcon,
|
|
@@ -24,7 +23,6 @@ import {
|
|
|
24
23
|
SettingsIcon,
|
|
25
24
|
StorageIcon,
|
|
26
25
|
SystemIcon,
|
|
27
|
-
TemplatesIcon,
|
|
28
26
|
ToolsIcon,
|
|
29
27
|
} from './nav-marks';
|
|
30
28
|
|
|
@@ -39,8 +37,6 @@ export const NAV_ICONS: Readonly<Record<Exclude<RouteToken, 'login'>, IconCompon
|
|
|
39
37
|
agents: AgentsIcon,
|
|
40
38
|
presets: PresetsIcon,
|
|
41
39
|
states: DatabaseIcon,
|
|
42
|
-
extensions: ExtensionsIcon,
|
|
43
|
-
templates: TemplatesIcon,
|
|
44
40
|
connectors: ConnectorsIcon,
|
|
45
41
|
servedEndpoints: ServedEndpointsIcon,
|
|
46
42
|
hooks: HooksIcon,
|
|
@@ -47,21 +47,6 @@ export const PresetsIcon: IconComponent = (props) => (
|
|
|
47
47
|
</Icon>
|
|
48
48
|
);
|
|
49
49
|
|
|
50
|
-
/** Extensions: a jigsaw piece — knob above, socket to the right. */
|
|
51
|
-
export const ExtensionsIcon: IconComponent = (props) => (
|
|
52
|
-
<Icon {...props}>
|
|
53
|
-
<path d="M9.5 6.5a2.2 2.2 0 0 1 4.4 0v.7h4.3a1.3 1.3 0 0 1 1.3 1.3v3.4H18a2.2 2.2 0 0 0 0 4.4h1.5v2.8a1.3 1.3 0 0 1-1.3 1.3H5.8a1.3 1.3 0 0 1-1.3-1.3V8.5a1.3 1.3 0 0 1 1.3-1.3h3.7z" />
|
|
54
|
-
</Icon>
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
/** Templates: a stack of two sheets, the front one offset. */
|
|
58
|
-
export const TemplatesIcon: IconComponent = (props) => (
|
|
59
|
-
<Icon {...props}>
|
|
60
|
-
<path d="M8 8V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-3" />
|
|
61
|
-
<rect x="3" y="8" width="13" height="13" rx="2" />
|
|
62
|
-
</Icon>
|
|
63
|
-
);
|
|
64
|
-
|
|
65
50
|
/** Connectors: a two-pin power plug on its lead. */
|
|
66
51
|
export const ConnectorsIcon: IconComponent = (props) => (
|
|
67
52
|
<Icon {...props}>
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single-select both `AgentPicker` and `ChannelPicker` render: one list read of
|
|
3
|
+
* names, drawn in its own `Field` with the read's states (pending, error with Retry,
|
|
4
|
+
* refused on a 403, loaded and empty) and a stored value the loaded list does not
|
|
5
|
+
* hold flagged with the picker's unlisted label. Each picker hands in its own copy.
|
|
6
|
+
* The host's `error` shows, and the `Field` claims its control id, exactly while a
|
|
7
|
+
* `Select` renders.
|
|
8
|
+
*/
|
|
9
|
+
import { type ReactNode, useId } from 'react';
|
|
10
|
+
|
|
11
|
+
import { errorMessage, isForbiddenError } from '../errors';
|
|
12
|
+
import { Badge } from './badge';
|
|
13
|
+
import { Field } from './field';
|
|
14
|
+
import { ErrorState } from './primitives';
|
|
15
|
+
import { Select, type SelectOption } from './select';
|
|
16
|
+
|
|
17
|
+
const MUTED_LINE = { margin: 0, color: 'var(--tai-color-text-muted)' } as const;
|
|
18
|
+
|
|
19
|
+
/** The flags of a list read; `data` is each picker's own. */
|
|
20
|
+
export interface ListReadFlags {
|
|
21
|
+
readonly isPending: boolean;
|
|
22
|
+
readonly isError: boolean;
|
|
23
|
+
readonly isSuccess: boolean;
|
|
24
|
+
readonly error: unknown;
|
|
25
|
+
readonly refetch: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The picker's own copy. */
|
|
29
|
+
export interface NamedListCopy {
|
|
30
|
+
readonly label: string;
|
|
31
|
+
readonly description?: string | undefined;
|
|
32
|
+
readonly loadingPlaceholder: string;
|
|
33
|
+
readonly placeholder: string;
|
|
34
|
+
readonly forbiddenNote: string;
|
|
35
|
+
readonly emptyNote: string;
|
|
36
|
+
readonly emptyAction: ReactNode;
|
|
37
|
+
readonly unlistedLabel: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface NamedListPickerProps {
|
|
41
|
+
readonly query: ListReadFlags;
|
|
42
|
+
/** The listed names once the read succeeded. */
|
|
43
|
+
readonly names: readonly string[];
|
|
44
|
+
readonly value: string;
|
|
45
|
+
readonly onValueChange: (value: string) => void;
|
|
46
|
+
readonly error?: string | undefined;
|
|
47
|
+
readonly copy: NamedListCopy;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function MutedLine({ text }: { readonly text: string }): ReactNode {
|
|
51
|
+
return (
|
|
52
|
+
<p role="status" style={MUTED_LINE}>
|
|
53
|
+
{text}
|
|
54
|
+
</p>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** What the picker draws, derived from the read, the listed names and the value. */
|
|
59
|
+
interface NamedListView {
|
|
60
|
+
readonly refused: boolean;
|
|
61
|
+
readonly failed: boolean;
|
|
62
|
+
readonly pending: boolean;
|
|
63
|
+
readonly unlisted: boolean;
|
|
64
|
+
readonly empty: boolean;
|
|
65
|
+
readonly options: readonly SelectOption[];
|
|
66
|
+
readonly selectRenders: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The stored value's own option: flagged when the loaded list does not hold it, and kept
|
|
70
|
+
* unflagged under a 403, where without the list the form claims nothing about it. */
|
|
71
|
+
function storedOptions(
|
|
72
|
+
stored: string,
|
|
73
|
+
unlisted: boolean,
|
|
74
|
+
refused: boolean,
|
|
75
|
+
unlistedLabel: string,
|
|
76
|
+
): SelectOption[] {
|
|
77
|
+
if (unlisted) return [{ value: stored, label: `${stored} (${unlistedLabel})` }];
|
|
78
|
+
return refused ? [{ value: stored, label: stored }] : [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function listedOptions(loaded: boolean, names: readonly string[]): SelectOption[] {
|
|
82
|
+
return loaded ? names.map((name) => ({ value: name, label: name })) : [];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function namedListView(
|
|
86
|
+
query: ListReadFlags,
|
|
87
|
+
names: readonly string[],
|
|
88
|
+
value: string,
|
|
89
|
+
unlistedLabel: string,
|
|
90
|
+
): NamedListView {
|
|
91
|
+
const refused = query.isError && isForbiddenError(query.error);
|
|
92
|
+
const failed = query.isError && !refused;
|
|
93
|
+
const loaded = query.isSuccess && !query.isError;
|
|
94
|
+
const pending = !loaded && !query.isError;
|
|
95
|
+
const stored = value !== '' ? value : null;
|
|
96
|
+
const unlisted = loaded && stored !== null && !names.includes(stored);
|
|
97
|
+
const empty = loaded && names.length === 0;
|
|
98
|
+
|
|
99
|
+
const options =
|
|
100
|
+
stored === null
|
|
101
|
+
? listedOptions(loaded, names)
|
|
102
|
+
: [
|
|
103
|
+
...storedOptions(stored, unlisted, refused, unlistedLabel),
|
|
104
|
+
...listedOptions(loaded, names),
|
|
105
|
+
];
|
|
106
|
+
const selectRenders = pending || (refused ? stored !== null : !failed && (!empty || unlisted));
|
|
107
|
+
return { refused, failed, pending, unlisted, empty, options, selectRenders };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The loaded, pending or refused body: the `Select` while one renders, then its notes. */
|
|
111
|
+
function NamedListBody({
|
|
112
|
+
view,
|
|
113
|
+
value,
|
|
114
|
+
onValueChange,
|
|
115
|
+
copy,
|
|
116
|
+
}: {
|
|
117
|
+
readonly view: NamedListView;
|
|
118
|
+
readonly value: string;
|
|
119
|
+
readonly onValueChange: (value: string) => void;
|
|
120
|
+
readonly copy: NamedListCopy;
|
|
121
|
+
}): ReactNode {
|
|
122
|
+
const unlistedId = useId();
|
|
123
|
+
return (
|
|
124
|
+
<>
|
|
125
|
+
{view.selectRenders ? (
|
|
126
|
+
<Select
|
|
127
|
+
options={view.options}
|
|
128
|
+
value={value}
|
|
129
|
+
placeholder={view.pending ? copy.loadingPlaceholder : copy.placeholder}
|
|
130
|
+
disabled={view.pending}
|
|
131
|
+
onValueChange={onValueChange}
|
|
132
|
+
aria-describedby={view.unlisted ? unlistedId : undefined}
|
|
133
|
+
/>
|
|
134
|
+
) : null}
|
|
135
|
+
{view.refused ? <MutedLine text={copy.forbiddenNote} /> : null}
|
|
136
|
+
{view.empty && !view.refused ? (
|
|
137
|
+
<>
|
|
138
|
+
<MutedLine text={copy.emptyNote} />
|
|
139
|
+
{copy.emptyAction}
|
|
140
|
+
</>
|
|
141
|
+
) : null}
|
|
142
|
+
{view.unlisted ? (
|
|
143
|
+
<p id={unlistedId} className="tai-field-hint" style={{ margin: 0 }}>
|
|
144
|
+
<Badge variant="warning">{copy.unlistedLabel}</Badge>{' '}
|
|
145
|
+
{`${value} is ${copy.unlistedLabel}. Pick another, or saving is refused.`}
|
|
146
|
+
</p>
|
|
147
|
+
) : null}
|
|
148
|
+
</>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function NamedListPicker({
|
|
153
|
+
query,
|
|
154
|
+
names,
|
|
155
|
+
value,
|
|
156
|
+
onValueChange,
|
|
157
|
+
error,
|
|
158
|
+
copy,
|
|
159
|
+
}: NamedListPickerProps): ReactNode {
|
|
160
|
+
const view = namedListView(query, names, value, copy.unlistedLabel);
|
|
161
|
+
return (
|
|
162
|
+
<Field
|
|
163
|
+
label={copy.label}
|
|
164
|
+
description={copy.description}
|
|
165
|
+
error={view.selectRenders ? error : undefined}
|
|
166
|
+
// Only the Select claims the field's control id; with nothing to hold no
|
|
167
|
+
// labelable element renders, so `for` would dangle.
|
|
168
|
+
group={!view.selectRenders}
|
|
169
|
+
>
|
|
170
|
+
{view.failed ? (
|
|
171
|
+
<ErrorState
|
|
172
|
+
message={errorMessage(query.error)}
|
|
173
|
+
onRetry={() => {
|
|
174
|
+
query.refetch();
|
|
175
|
+
}}
|
|
176
|
+
/>
|
|
177
|
+
) : (
|
|
178
|
+
<NamedListBody view={view} value={value} onValueChange={onValueChange} copy={copy} />
|
|
179
|
+
)}
|
|
180
|
+
</Field>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** The stub query and navigation harness the agent and channel picker tests share. */
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import { vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { NavigationProvider } from '../navigation/context';
|
|
7
|
+
import type { NavigationContextValue, RouteSearch, RouteToken } from '../navigation/types';
|
|
8
|
+
|
|
9
|
+
export interface StubQuery<T> {
|
|
10
|
+
readonly data: T | undefined;
|
|
11
|
+
readonly isPending: boolean;
|
|
12
|
+
readonly isError: boolean;
|
|
13
|
+
readonly isSuccess: boolean;
|
|
14
|
+
readonly error: unknown;
|
|
15
|
+
readonly refetch: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function stubQuery<T>(overrides: Partial<StubQuery<T>>): StubQuery<T> {
|
|
19
|
+
return {
|
|
20
|
+
data: undefined,
|
|
21
|
+
isPending: false,
|
|
22
|
+
isError: false,
|
|
23
|
+
isSuccess: false,
|
|
24
|
+
error: null,
|
|
25
|
+
refetch: vi.fn(),
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const pendingRead = { isPending: true } as const;
|
|
31
|
+
export const forbiddenRead = { isError: true, error: { status: 403 } } as const;
|
|
32
|
+
|
|
33
|
+
function navigation(): NavigationContextValue {
|
|
34
|
+
return {
|
|
35
|
+
navigate: vi.fn(),
|
|
36
|
+
resolvePath: <T extends RouteToken>(token: T, search?: RouteSearch<T>) => {
|
|
37
|
+
const qs = search ? new URLSearchParams(search as Record<string, string>).toString() : '';
|
|
38
|
+
return `/${token}${qs ? `?${qs}` : ''}`;
|
|
39
|
+
},
|
|
40
|
+
navigatePlugin: vi.fn(),
|
|
41
|
+
resolvePluginPath: vi.fn(() => '/plugins'),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function renderWithNavigation(ui: ReactNode): ReturnType<typeof render> {
|
|
46
|
+
return render(<NavigationProvider value={navigation()}>{ui}</NavigationProvider>);
|
|
47
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The report of one fleet recycle roll — shared by the profile apply report and the
|
|
3
|
+
* Environment tab's save report, so both read the same rows the same way.
|
|
4
|
+
*
|
|
5
|
+
* One row per worker the roll reached: its name, kind, status and life (joined onto a
|
|
6
|
+
* fresh life sharing its name). `recycled` reads as done, the writer's own
|
|
7
|
+
* `self-deferred` line as a warning with its note, and the target the roll stopped at
|
|
8
|
+
* (`timed-out` / `failed`) as a failure with the reason the server gave. When the roll
|
|
9
|
+
* stopped, a warning panel says where and why and that the workers after it still run
|
|
10
|
+
* the old value. Names only — the report carries no env value. Every server string
|
|
11
|
+
* renders as escaped React text.
|
|
12
|
+
*/
|
|
13
|
+
import type { FreshLife, RecycleEntry, RecycleStop } from '@tai42/api-client';
|
|
14
|
+
import type { CSSProperties, ReactNode } from 'react';
|
|
15
|
+
|
|
16
|
+
import { Badge } from './badge';
|
|
17
|
+
import { AlertTriangleIcon } from './icons';
|
|
18
|
+
|
|
19
|
+
const labelStyle: CSSProperties = {
|
|
20
|
+
fontSize: 'var(--tai-text-sm)',
|
|
21
|
+
color: 'var(--tai-color-text-muted)',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const rowListStyle: CSSProperties = {
|
|
25
|
+
listStyle: 'none',
|
|
26
|
+
margin: 0,
|
|
27
|
+
padding: 0,
|
|
28
|
+
display: 'flex',
|
|
29
|
+
flexDirection: 'column',
|
|
30
|
+
gap: 'var(--tai-space-3)',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const noteStyle: CSSProperties = {
|
|
34
|
+
margin: '0 0 var(--tai-space-3)',
|
|
35
|
+
color: 'var(--tai-color-text-muted)',
|
|
36
|
+
fontSize: 'var(--tai-text-sm)',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** The badge tone a row's status takes: done, the writer's deferred exit, or the stop. */
|
|
40
|
+
function statusVariant(status: string): string {
|
|
41
|
+
if (status === 'recycled') return 'success';
|
|
42
|
+
if (status === 'self-deferred') return 'warning';
|
|
43
|
+
return 'danger';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Where the roll stopped and why: the workers after it were not recycled. */
|
|
47
|
+
function StoppedPanel({ stopped }: { readonly stopped: RecycleStop }): ReactNode {
|
|
48
|
+
const where =
|
|
49
|
+
stopped.name !== null
|
|
50
|
+
? `The recycle stopped at ${stopped.name} (${stopped.kind} workers)`
|
|
51
|
+
: `The recycle stopped before it reached a worker (${stopped.kind} workers)`;
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
role="alert"
|
|
55
|
+
className="tai-warn-state tai-stack tai-stack-2"
|
|
56
|
+
data-testid="recycle-stopped"
|
|
57
|
+
>
|
|
58
|
+
<strong className="tai-status tai-status-warn">
|
|
59
|
+
<AlertTriangleIcon />
|
|
60
|
+
{where}
|
|
61
|
+
</strong>
|
|
62
|
+
<p>{stopped.detail}</p>
|
|
63
|
+
<p>
|
|
64
|
+
The change is saved and every worker reloaded, but the workers after this point were not
|
|
65
|
+
recycled and still run the old value, and this worker was not recycled. Restart them through
|
|
66
|
+
your deployment’s supervisor.
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface RecycleRollSectionProps {
|
|
73
|
+
readonly recycle: readonly RecycleEntry[];
|
|
74
|
+
readonly fresh: readonly FreshLife[];
|
|
75
|
+
readonly stopped: RecycleStop | null;
|
|
76
|
+
/** The note beside the worker that took the change, on its `self-deferred` row. */
|
|
77
|
+
readonly writerNote: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function RecycleRollSection({
|
|
81
|
+
recycle,
|
|
82
|
+
fresh,
|
|
83
|
+
stopped,
|
|
84
|
+
writerNote,
|
|
85
|
+
}: RecycleRollSectionProps): ReactNode {
|
|
86
|
+
if (recycle.length === 0 && stopped === null) {
|
|
87
|
+
return <p style={noteStyle}>No process recycle was needed.</p>;
|
|
88
|
+
}
|
|
89
|
+
return (
|
|
90
|
+
<div className="tai-stack">
|
|
91
|
+
{recycle.length > 0 ? (
|
|
92
|
+
<ul style={rowListStyle} data-testid="apply-recycle">
|
|
93
|
+
{recycle.map((entry) => {
|
|
94
|
+
const joined = fresh.find((life) => life.name === entry.name);
|
|
95
|
+
return (
|
|
96
|
+
<li
|
|
97
|
+
key={`${entry.name}-${entry.kind}-${String(entry.generation_before)}`}
|
|
98
|
+
className="tai-row"
|
|
99
|
+
>
|
|
100
|
+
<span className="tai-mono">{entry.name}</span>
|
|
101
|
+
<Badge variant="neutral">{entry.kind}</Badge>
|
|
102
|
+
<Badge variant={statusVariant(entry.status)}>{entry.status}</Badge>
|
|
103
|
+
<span style={labelStyle}>
|
|
104
|
+
{joined !== undefined
|
|
105
|
+
? `life ${String(entry.generation_before)} to ${String(joined.generation)}`
|
|
106
|
+
: `life ${String(entry.generation_before)}`}
|
|
107
|
+
</span>
|
|
108
|
+
{entry.status === 'self-deferred' ? (
|
|
109
|
+
<span style={labelStyle}>{writerNote}</span>
|
|
110
|
+
) : null}
|
|
111
|
+
{entry.detail !== null ? <span style={labelStyle}>{entry.detail}</span> : null}
|
|
112
|
+
</li>
|
|
113
|
+
);
|
|
114
|
+
})}
|
|
115
|
+
</ul>
|
|
116
|
+
) : null}
|
|
117
|
+
{stopped !== null ? <StoppedPanel stopped={stopped} /> : null}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Stepper` — the progress of a short multi-step flow: a named `<nav>` over an
|
|
3
|
+
* ordered list, one entry per step with a numbered (or marked) 24 px marker and
|
|
4
|
+
* its label. The `current` step carries `aria-current="step"`; every marker says
|
|
5
|
+
* its state in visually-hidden text, so a state is never told by colour alone.
|
|
6
|
+
*
|
|
7
|
+
* A `done` step is a button when `onStepSelect` is given (Tab reaches it,
|
|
8
|
+
* Enter/Space selects it); without it no step is interactive. The stepper holds
|
|
9
|
+
* no step content: the caller renders the current step's body.
|
|
10
|
+
*
|
|
11
|
+
* At the narrow breakpoint the list collapses to one line, "Step 2 of 3: Pick
|
|
12
|
+
* tools", with the other steps visually hidden (still in the list).
|
|
13
|
+
*/
|
|
14
|
+
import type { ReactNode } from 'react';
|
|
15
|
+
|
|
16
|
+
import { AlertTriangleIcon, CheckIcon } from './icons';
|
|
17
|
+
|
|
18
|
+
export type StepperStatus = 'done' | 'current' | 'upcoming' | 'error';
|
|
19
|
+
|
|
20
|
+
export interface StepperStep {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly label: string;
|
|
23
|
+
readonly status: StepperStatus;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface StepperProps {
|
|
27
|
+
readonly steps: readonly StepperStep[];
|
|
28
|
+
readonly 'aria-label': string;
|
|
29
|
+
/** Called with a `done` step's id; when omitted no step is interactive. */
|
|
30
|
+
readonly onStepSelect?: (id: string) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const STATUS_TEXT: Record<StepperStatus, string> = {
|
|
34
|
+
done: 'completed',
|
|
35
|
+
current: 'current step',
|
|
36
|
+
upcoming: 'not started',
|
|
37
|
+
error: 'failed',
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const MARKER_CLASS: Record<StepperStatus, string> = {
|
|
41
|
+
done: 'tai-stepper-marker tai-stepper-marker-done',
|
|
42
|
+
current: 'tai-stepper-marker tai-stepper-marker-current',
|
|
43
|
+
upcoming: 'tai-stepper-marker tai-stepper-marker-upcoming',
|
|
44
|
+
error: 'tai-stepper-marker tai-stepper-marker-error',
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const STEP_CLASS: Record<StepperStatus, string> = {
|
|
48
|
+
done: 'tai-stepper-step tai-stepper-step-done',
|
|
49
|
+
current: 'tai-stepper-step tai-stepper-step-current',
|
|
50
|
+
upcoming: 'tai-stepper-step tai-stepper-step-upcoming',
|
|
51
|
+
error: 'tai-stepper-step',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function Marker({ status, index }: { status: StepperStatus; index: number }): ReactNode {
|
|
55
|
+
return (
|
|
56
|
+
<span className={MARKER_CLASS[status]} aria-hidden="true">
|
|
57
|
+
{status === 'done' ? (
|
|
58
|
+
<CheckIcon />
|
|
59
|
+
) : status === 'error' ? (
|
|
60
|
+
<AlertTriangleIcon />
|
|
61
|
+
) : (
|
|
62
|
+
String(index + 1)
|
|
63
|
+
)}
|
|
64
|
+
</span>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function Stepper({ steps, 'aria-label': label, onStepSelect }: StepperProps): ReactNode {
|
|
69
|
+
const currentIndex = steps.findIndex((step) => step.status === 'current');
|
|
70
|
+
return (
|
|
71
|
+
<nav aria-label={label}>
|
|
72
|
+
<ol className="tai-stepper">
|
|
73
|
+
{steps.map((step, index) => {
|
|
74
|
+
const body = (
|
|
75
|
+
<>
|
|
76
|
+
<Marker status={step.status} index={index} />
|
|
77
|
+
<span className="tai-stepper-label">
|
|
78
|
+
{currentIndex === index ? (
|
|
79
|
+
<span className="tai-stepper-position">{`Step ${String(index + 1)} of ${String(steps.length)}: `}</span>
|
|
80
|
+
) : null}
|
|
81
|
+
{step.label}
|
|
82
|
+
</span>
|
|
83
|
+
<span className="tai-visually-hidden">{`, ${STATUS_TEXT[step.status]}`}</span>
|
|
84
|
+
</>
|
|
85
|
+
);
|
|
86
|
+
return (
|
|
87
|
+
<li
|
|
88
|
+
key={step.id}
|
|
89
|
+
className={STEP_CLASS[step.status]}
|
|
90
|
+
aria-current={step.status === 'current' ? 'step' : undefined}
|
|
91
|
+
>
|
|
92
|
+
{step.status === 'done' && onStepSelect !== undefined ? (
|
|
93
|
+
<button
|
|
94
|
+
type="button"
|
|
95
|
+
className="tai-stepper-button"
|
|
96
|
+
onClick={() => {
|
|
97
|
+
onStepSelect(step.id);
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
{body}
|
|
101
|
+
</button>
|
|
102
|
+
) : (
|
|
103
|
+
<span className="tai-stepper-button">{body}</span>
|
|
104
|
+
)}
|
|
105
|
+
</li>
|
|
106
|
+
);
|
|
107
|
+
})}
|
|
108
|
+
</ol>
|
|
109
|
+
</nav>
|
|
110
|
+
);
|
|
111
|
+
}
|