@artemis-studio/plugin-sdk 2026.9.37
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 +38 -0
- package/index.js +25 -0
- package/package.json +46 -0
- package/peers.json +8 -0
- package/shared.d.ts +3 -0
- package/shared.js +24 -0
- package/types/branding.d.ts +14 -0
- package/types/kernel/api/polling.d.ts +50 -0
- package/types/kernel/api/request.d.ts +45 -0
- package/types/kernel/api/schema.d.ts +9428 -0
- package/types/kernel/auth/Can.d.ts +9 -0
- package/types/kernel/auth/LoginView.d.ts +8 -0
- package/types/kernel/auth/api.d.ts +29 -0
- package/types/kernel/auth/useCan.d.ts +15 -0
- package/types/kernel/feature.d.ts +80 -0
- package/types/kernel/features.d.ts +5 -0
- package/types/kernel/manifest.d.ts +21 -0
- package/types/kernel/nav/groups.d.ts +22 -0
- package/types/kernel/plugins/PluginBoundary.d.ts +25 -0
- package/types/kernel/plugins/PluginUnavailable.d.ts +6 -0
- package/types/kernel/plugins/boot.d.ts +29 -0
- package/types/kernel/plugins/guarded.d.ts +3 -0
- package/types/kernel/plugins/usePluginsChanged.d.ts +6 -0
- package/types/kernel/plugins/validate.d.ts +19 -0
- package/types/kernel/registry.d.ts +12 -0
- package/types/kernel/routing/roots.d.ts +15 -0
- package/types/kernel/shell/AccountView.d.ts +9 -0
- package/types/kernel/shell/AdminView.d.ts +5 -0
- package/types/kernel/shell/ClusterLayout.d.ts +10 -0
- package/types/kernel/shell/ClusterViewNav.d.ts +12 -0
- package/types/kernel/shell/CommandPalette.d.ts +10 -0
- package/types/kernel/shell/FeatureDisabled.d.ts +13 -0
- package/types/kernel/shell/FeatureGate.d.ts +12 -0
- package/types/kernel/shell/FreshnessBar.d.ts +11 -0
- package/types/kernel/shell/HomeView.d.ts +5 -0
- package/types/kernel/shell/NavItem.d.ts +22 -0
- package/types/kernel/shell/NavToggle.d.ts +6 -0
- package/types/kernel/shell/RootLayout.d.ts +12 -0
- package/types/kernel/shell/UserMenu.d.ts +6 -0
- package/types/kernel/shell/useFreshness.d.ts +25 -0
- package/types/kernel/shell/useNavCollapsed.d.ts +10 -0
- package/types/kernel/slots.d.ts +130 -0
- package/types/kernel/stream/useClusterStream.d.ts +22 -0
- package/types/kernel/time/time.d.ts +89 -0
- package/types/kernel/time/timezone.d.ts +44 -0
- package/types/kernel/useDismissedNotice.d.ts +3 -0
- package/types/sdk/index.d.ts +44 -0
- package/types/ui/ConfirmByTyping.d.ts +15 -0
- package/types/ui/NodeOutcomeSummary.d.ts +69 -0
- package/types/ui/Pager.d.ts +19 -0
- package/types/ui/VirtualTable.d.ts +56 -0
- package/vite.d.ts +14 -0
- package/vite.js +60 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
/** Queues picked by name, or every queue matching the queues screen's filter (`q`, blank for all). */
|
|
3
|
+
export type QueueSelection = {
|
|
4
|
+
kind: 'names';
|
|
5
|
+
names: string[];
|
|
6
|
+
} | {
|
|
7
|
+
kind: 'filter';
|
|
8
|
+
q: string;
|
|
9
|
+
total: number;
|
|
10
|
+
};
|
|
11
|
+
/** Messages picked by id, every message matching the messages screen's selector, or the whole queue. */
|
|
12
|
+
export type MessageSelection = {
|
|
13
|
+
kind: 'ids';
|
|
14
|
+
ids: number[];
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'filter';
|
|
17
|
+
filter: string;
|
|
18
|
+
} | {
|
|
19
|
+
kind: 'all';
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The kernel-owned slots and what each hands its contributions (ADR-0070). A slot lets a screen
|
|
23
|
+
* show another feature's panel without importing that feature, and without a placeholder when the
|
|
24
|
+
* feature is disabled.
|
|
25
|
+
*/
|
|
26
|
+
export interface SlotProps {
|
|
27
|
+
/** In the application header, after the product name: status across clusters that needs attention. */
|
|
28
|
+
'shell.header': object;
|
|
29
|
+
/** In the sidebar, above the open cluster's views: how the operator moves between clusters. */
|
|
30
|
+
'shell.navbar': {
|
|
31
|
+
collapsed: boolean;
|
|
32
|
+
};
|
|
33
|
+
/** The landing page, when no cluster is open. */
|
|
34
|
+
'home.empty': object;
|
|
35
|
+
/** Above every view of a cluster: what identifies it, and what needs saying about its state. */
|
|
36
|
+
'cluster.header': {
|
|
37
|
+
clusterId: string;
|
|
38
|
+
};
|
|
39
|
+
/** Below a passing registration check: what the enabled features added to it, by feature id. */
|
|
40
|
+
'cluster.registration.afterProbe': {
|
|
41
|
+
contributions: Record<string, unknown>;
|
|
42
|
+
};
|
|
43
|
+
/** In a queue's detail drawer, below its per-node breakdown. `onClose` closes the drawer before navigating. */
|
|
44
|
+
'queue.detail.panels': {
|
|
45
|
+
clusterId: string;
|
|
46
|
+
queueName: string;
|
|
47
|
+
onClose: () => void;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Beside the queues screen's selection: what can be done to the selected queues. `count` is how
|
|
51
|
+
* many are selected, and may be zero: an action then stays visible and disabled. `clear` empties
|
|
52
|
+
* the selection.
|
|
53
|
+
*/
|
|
54
|
+
'queues.selection': {
|
|
55
|
+
clusterId: string;
|
|
56
|
+
selection: QueueSelection;
|
|
57
|
+
count: number;
|
|
58
|
+
clear: () => void;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Beside the messages screen's selection: what can be done to the selected messages elsewhere.
|
|
62
|
+
* `node` is the Studio node being browsed, absent for the live node; `total` is how many messages
|
|
63
|
+
* the selection holds, null when the screen does not know. `clear` empties the selection.
|
|
64
|
+
*/
|
|
65
|
+
'messages.selection': {
|
|
66
|
+
clusterId: string;
|
|
67
|
+
queueName: string;
|
|
68
|
+
node?: string;
|
|
69
|
+
selection: MessageSelection;
|
|
70
|
+
total: number | null;
|
|
71
|
+
clear: () => void;
|
|
72
|
+
};
|
|
73
|
+
/** At the foot of a cluster's metrics view. */
|
|
74
|
+
'metrics.panels': {
|
|
75
|
+
clusterId: string;
|
|
76
|
+
};
|
|
77
|
+
/** Inside a box on the topology graph, after its name. `nodeIds` are the broker endpoints the box stands for. */
|
|
78
|
+
'topology.node.marks': {
|
|
79
|
+
clusterId: string;
|
|
80
|
+
nodeIds: string[];
|
|
81
|
+
};
|
|
82
|
+
/** A section of a cluster's Settings page, under the contribution's title. */
|
|
83
|
+
'settings.sections': {
|
|
84
|
+
clusterId: string;
|
|
85
|
+
};
|
|
86
|
+
/** A tab of a cluster's Routing page, after Diverts and Bridges, labelled with the contribution's title; its id is the tab's `?tab=`. */
|
|
87
|
+
'routing.tabs': {
|
|
88
|
+
clusterId: string;
|
|
89
|
+
};
|
|
90
|
+
/** A tab of the Administration page, labelled with the contribution's title; its id is the tab's `?tab=`. */
|
|
91
|
+
'admin.tabs': object;
|
|
92
|
+
/** A section of the signed-in user's Account page, under the contribution's title. */
|
|
93
|
+
'account.sections': object;
|
|
94
|
+
}
|
|
95
|
+
export type SlotName = keyof SlotProps;
|
|
96
|
+
/**
|
|
97
|
+
* The closed, ordered headings a Settings page groups its tabs under, in the order an operator's
|
|
98
|
+
* reach widens: their own preferences, then what is shared across Studio, then this cluster, then
|
|
99
|
+
* what plugins added. Adding one is a kernel change, like a navigation group (ADR-0070).
|
|
100
|
+
*/
|
|
101
|
+
export declare const SETTINGS_GROUPS: readonly [{
|
|
102
|
+
readonly id: "personal";
|
|
103
|
+
readonly label: "Yours";
|
|
104
|
+
}, {
|
|
105
|
+
readonly id: "studio";
|
|
106
|
+
readonly label: "Studio";
|
|
107
|
+
}, {
|
|
108
|
+
readonly id: "cluster";
|
|
109
|
+
readonly label: "This cluster";
|
|
110
|
+
}, {
|
|
111
|
+
readonly id: "plugins";
|
|
112
|
+
readonly label: "Plugins";
|
|
113
|
+
}];
|
|
114
|
+
export type SettingsGroupId = (typeof SETTINGS_GROUPS)[number]['id'];
|
|
115
|
+
export interface SlotContribution<P> {
|
|
116
|
+
/** Unique within the slot. */
|
|
117
|
+
id: string;
|
|
118
|
+
/** Position within the slot; lower comes first. */
|
|
119
|
+
order: number;
|
|
120
|
+
/** The heading or tab label, in the slots that show one. */
|
|
121
|
+
title?: string;
|
|
122
|
+
/** `settings.sections` only: the heading its tab sits under. Without one it is listed under Plugins. */
|
|
123
|
+
group?: SettingsGroupId;
|
|
124
|
+
Component: ComponentType<P>;
|
|
125
|
+
}
|
|
126
|
+
export type SlotContributions = {
|
|
127
|
+
[K in SlotName]?: SlotContribution<SlotProps[K]>[];
|
|
128
|
+
};
|
|
129
|
+
/** The enabled features' contributions to one slot, in order. */
|
|
130
|
+
export declare function useSlot<K extends SlotName>(name: K): SlotContribution<SlotProps[K]>[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** What the UI reports about the live connection (ADR-0052). */
|
|
2
|
+
export type StreamStatus = 'connecting' | 'live' | 'reconnecting' | 'offline';
|
|
3
|
+
/** The live-stream state, or `null` on a route that mounts no stream. */
|
|
4
|
+
export declare function useStreamStatus(): StreamStatus | null;
|
|
5
|
+
/**
|
|
6
|
+
* One `EventSource` per mounted cluster view (ADR-0003, ADR-0018, ADR-0027).
|
|
7
|
+
*
|
|
8
|
+
* - Each topic's frames go to the handler its feature contributes (ADR-0070). A
|
|
9
|
+
* signal topic's handler invalidates the matching TanStack Query keys and the
|
|
10
|
+
* normal `queryFn` refetches; a topic of a disabled feature has no handler.
|
|
11
|
+
* - `onFrame` also hands the view every frame of the topics it mounted, for a topic
|
|
12
|
+
* that carries data with no server-side resource behind it, such as the live
|
|
13
|
+
* broker-event feed. The browser echoes the last `id:` back as `Last-Event-ID`
|
|
14
|
+
* on reconnect, so missed events replay automatically.
|
|
15
|
+
*
|
|
16
|
+
* It reconnects indefinitely with capped exponential backoff and full jitter, and
|
|
17
|
+
* treats silence as failure (ADR-0052): an intermediary that drops the connection
|
|
18
|
+
* without a clean close fires no `error`, so the only way to notice is to miss a
|
|
19
|
+
* keep-alive. The returned status is what the freshness indicator reports; the
|
|
20
|
+
* per-hook `refetchInterval` keeps every view updating while it is not `live`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useClusterStream(clusterId: string, topics: string[], onFrame?: (topic: string, data: string) => void): StreamStatus;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask the server what time it is and fold the answer in.
|
|
3
|
+
*
|
|
4
|
+
* Failures are silent on purpose: the probe is not a screen and has no operator
|
|
5
|
+
* waiting on it. A 401 or a dropped network leaves the previous estimate standing,
|
|
6
|
+
* which is strictly better than reverting to the browser's clock, and the real
|
|
7
|
+
* queries on the screen are what report that the server is unreachable.
|
|
8
|
+
*
|
|
9
|
+
* Concurrent callers join the request in flight rather than starting another.
|
|
10
|
+
*/
|
|
11
|
+
export declare function syncServerTime(): Promise<void>;
|
|
12
|
+
/**
|
|
13
|
+
* The SSE keep-alive as a drift detector — never as a measurement.
|
|
14
|
+
*
|
|
15
|
+
* `SseHub.heartbeat` puts the server's clock in every `ping`, so the client is
|
|
16
|
+
* told the time every twenty seconds for free. It is one-way with unmeasurable
|
|
17
|
+
* latency, so it cannot teach the estimator; what it can do is notice that the
|
|
18
|
+
* estimate has gone wrong and ask for a real probe.
|
|
19
|
+
*/
|
|
20
|
+
export declare function offerPing(data: unknown): void;
|
|
21
|
+
/** Studio's clock, as best the browser can tell. The only wall-clock read in the app. */
|
|
22
|
+
export declare function serverNow(): number;
|
|
23
|
+
/**
|
|
24
|
+
* Put a browser-stamped instant on Studio's timeline.
|
|
25
|
+
*
|
|
26
|
+
* The only such instants are TanStack Query's `dataUpdatedAt`, which it stamps
|
|
27
|
+
* with its own `Date.now()` when a response lands. Comparing one against
|
|
28
|
+
* {@link serverNow} without this would reintroduce exactly the error this module
|
|
29
|
+
* removes — and silently, because the number looks right.
|
|
30
|
+
*/
|
|
31
|
+
export declare function toServerMs(browserMs: number): number;
|
|
32
|
+
/** How far the browser's clock is from Studio's, in ms. Positive means the browser is ahead. */
|
|
33
|
+
export declare function browserOffsetMs(): number;
|
|
34
|
+
/**
|
|
35
|
+
* Keep the estimate honest for as long as the tab lives.
|
|
36
|
+
*
|
|
37
|
+
* Three triggers, because clocks go wrong in three ways: they drift (the
|
|
38
|
+
* interval), they are corrected or the machine wakes from sleep (the step check
|
|
39
|
+
* and `visibilitychange`), and the tab is backgrounded long enough that neither
|
|
40
|
+
* timer ran (`visibilitychange` again — browsers throttle background timers).
|
|
41
|
+
*
|
|
42
|
+
* The step check is the client-side `MonotonicClockWatch`: `performance.now()`
|
|
43
|
+
* advances monotonically, so comparing its delta against the wall clock's
|
|
44
|
+
* separates a jump from ordinary drift. A step means the offset is now wrong by
|
|
45
|
+
* the size of the jump, and unlearning that slowly through the EWMA would leave
|
|
46
|
+
* every label wrong in the meantime.
|
|
47
|
+
*
|
|
48
|
+
* Called once from the application root. Returns a teardown for tests.
|
|
49
|
+
*/
|
|
50
|
+
export declare function startServerTimeSync(): () => void;
|
|
51
|
+
/**
|
|
52
|
+
* A clock that ticks to re-render a relative label, on Studio's time.
|
|
53
|
+
*
|
|
54
|
+
* Also re-renders when the offset itself changes, which matters more than it
|
|
55
|
+
* looks: `MetricsView` ticks at the metric bucket width, up to an hour, and would
|
|
56
|
+
* otherwise hold a window computed before the first probe resolved for that long.
|
|
57
|
+
*/
|
|
58
|
+
export declare function useServerNow(intervalMs?: number): number;
|
|
59
|
+
/**
|
|
60
|
+
* `4s`, `3m`, `2h`, `1d` — short enough to sit in a header or a table cell.
|
|
61
|
+
*
|
|
62
|
+
* Floors rather than rounds. An age is a lower bound on how long ago something
|
|
63
|
+
* happened, and `59s` reading as `1m` overstates it; the operator reading a
|
|
64
|
+
* staleness label before a destructive action is the reason to prefer the
|
|
65
|
+
* conservative direction.
|
|
66
|
+
*/
|
|
67
|
+
export declare function elapsedLabel(ms: number): string;
|
|
68
|
+
/**
|
|
69
|
+
* A server instant, written down in the operator's chosen timezone.
|
|
70
|
+
*
|
|
71
|
+
* Fixed-width `YYYY-MM-DD HH:mm:ss` so a column of these sorts and scans by eye,
|
|
72
|
+
* and **always suffixed with the zone it is in** — `Z` for UTC, `+03:30`
|
|
73
|
+
* otherwise. The suffix is not decoration: the whole reason an operator picks a
|
|
74
|
+
* zone is to line a screen up against something else, and a timestamp that does
|
|
75
|
+
* not say which offset it is in is the one way this feature could mislead them.
|
|
76
|
+
*
|
|
77
|
+
* The offset is computed for *that instant*, not for now, so an event from last
|
|
78
|
+
* winter carries the offset that was in force when it happened.
|
|
79
|
+
*
|
|
80
|
+
* Accepts both wire shapes: the ISO-8601 strings every DTO carries, and the
|
|
81
|
+
* epoch-millis a broker's own message timestamps arrive as. Those are `0` when the
|
|
82
|
+
* broker set none, which is not a time and is rendered as such.
|
|
83
|
+
*
|
|
84
|
+
* Reads the zone from module state rather than a hook so it can be called from a
|
|
85
|
+
* table's column accessor. The consequence is that a view rendering one of these
|
|
86
|
+
* must subscribe with `useDisplayZone()` or it will not repaint when the zone
|
|
87
|
+
* changes.
|
|
88
|
+
*/
|
|
89
|
+
export declare function absoluteLabel(value: string | number | null | undefined): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Follow the browser, re-read every time rather than captured at load. */
|
|
2
|
+
export declare const AUTO = "auto";
|
|
3
|
+
export declare const DEFAULT_ZONE = "auto";
|
|
4
|
+
/** Whatever the browser currently believes its zone to be. */
|
|
5
|
+
export declare function localZone(): string;
|
|
6
|
+
/**
|
|
7
|
+
* What the operator chose, which may be {@link AUTO}. This is what the picker
|
|
8
|
+
* shows; {@link displayZone} is what formatters use.
|
|
9
|
+
*/
|
|
10
|
+
export declare function displayZonePreference(): string;
|
|
11
|
+
/** The zone to render instants in. Read synchronously, so formatters need no hook. */
|
|
12
|
+
export declare function displayZone(): string;
|
|
13
|
+
export declare function setDisplayZone(next: string): void;
|
|
14
|
+
/**
|
|
15
|
+
* Subscribe a view to the display zone.
|
|
16
|
+
*
|
|
17
|
+
* Any component that renders an absolute timestamp must call this, even if it
|
|
18
|
+
* ignores the returned value: `absoluteLabel` reads the zone from module state so
|
|
19
|
+
* it can be used inside a table's column accessor, which means nothing re-renders
|
|
20
|
+
* on a zone change unless something subscribed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useDisplayZone(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Every zone the runtime knows, UTC and the operator's own first.
|
|
25
|
+
*
|
|
26
|
+
* `Intl.supportedValuesOf` is the runtime's own IANA list, so it stays current
|
|
27
|
+
* without this project shipping a copy of the tz database. Where it is missing,
|
|
28
|
+
* the short list is still enough to be useful rather than empty.
|
|
29
|
+
*/
|
|
30
|
+
export declare function zoneOptions(): {
|
|
31
|
+
group: string;
|
|
32
|
+
items: {
|
|
33
|
+
value: string;
|
|
34
|
+
label: string;
|
|
35
|
+
}[];
|
|
36
|
+
}[];
|
|
37
|
+
/**
|
|
38
|
+
* `+03:30`, or `UTC` — the suffix that keeps a rendered timestamp unambiguous.
|
|
39
|
+
*
|
|
40
|
+
* Computed for a given instant, not for now: half the world changes offset twice
|
|
41
|
+
* a year, and an event from last winter must be labelled with the offset that was
|
|
42
|
+
* in force when it happened, not the one in force today.
|
|
43
|
+
*/
|
|
44
|
+
export declare function zoneSuffix(ms: number, inZone?: string): string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@artemis-studio/plugin-sdk` — everything a plugin's UI may use from Studio (ADR-0100). The host
|
|
3
|
+
* shares this module as a singleton, so a plugin's bundle uses the running Studio's own copy:
|
|
4
|
+
* the same router roots, the same query cache, the same permission checks. Nothing outside this
|
|
5
|
+
* file is part of the contract; a plugin that reaches past it breaks on the next Studio release.
|
|
6
|
+
*/
|
|
7
|
+
import type { ComponentType, ReactElement } from 'react';
|
|
8
|
+
import { CONTRACT, type PluginId, type StudioFeature } from '../kernel/feature.ts';
|
|
9
|
+
export { CONTRACT };
|
|
10
|
+
export type { NavContribution, PaletteSource, PluginId, RouteContributions, StudioFeature, TopicHandler, } from '../kernel/feature.ts';
|
|
11
|
+
export { SETTINGS_GROUPS, type MessageSelection, type QueueSelection, type SettingsGroupId, type SlotContribution, type SlotContributions, type SlotName, type SlotProps, } from '../kernel/slots.ts';
|
|
12
|
+
export { NAV_GROUPS, type NavGroupId } from '../kernel/nav/groups.ts';
|
|
13
|
+
export { clusterRoute, rootRoute } from '../kernel/routing/roots.ts';
|
|
14
|
+
export { ApiError, clusterKey, request } from '../kernel/api/request.ts';
|
|
15
|
+
export { useCan } from '../kernel/auth/useCan.ts';
|
|
16
|
+
export { useMe } from '../kernel/auth/api.ts';
|
|
17
|
+
export { ConfirmByTyping } from '../ui/ConfirmByTyping.tsx';
|
|
18
|
+
export { NodeOutcomeSummary, OutcomeSummary, type OutcomeRow } from '../ui/NodeOutcomeSummary.tsx';
|
|
19
|
+
export { Pager } from '../ui/Pager.tsx';
|
|
20
|
+
export { VirtualTable, type GridColumn } from '../ui/VirtualTable.tsx';
|
|
21
|
+
/**
|
|
22
|
+
* Shows a notification in Studio's own notification area. Import this, never
|
|
23
|
+
* `@mantine/notifications` directly: that is not shared, so a plugin's own copy would show nothing.
|
|
24
|
+
*/
|
|
25
|
+
export { notifications as notify } from '@mantine/notifications';
|
|
26
|
+
/** A plugin's description of itself: a {@link StudioFeature} whose id is the plugin's own. */
|
|
27
|
+
export interface StudioPlugin extends StudioFeature {
|
|
28
|
+
id: PluginId;
|
|
29
|
+
}
|
|
30
|
+
/** Declares a plugin's UI; its bundle exposes the result as `./feature`'s default export. */
|
|
31
|
+
export declare function definePlugin<T extends StudioPlugin>(plugin: T): T;
|
|
32
|
+
/**
|
|
33
|
+
* A route path under the plugin's own namespace, the only place its routes may live:
|
|
34
|
+
* `pluginPath('acme-notes', 'notes/$noteId')` is `p/acme-notes/notes/$noteId`. Use it with
|
|
35
|
+
* `rootRoute` for a page outside any cluster, or `clusterRoute` for a view of one cluster.
|
|
36
|
+
*/
|
|
37
|
+
export declare function pluginPath(id: PluginId, path?: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* The API path of a plugin's own endpoints, for {@link request}: `/p/<id>/…`, or
|
|
40
|
+
* `/clusters/<clusterId>/p/<id>/…` for a cluster-scoped call.
|
|
41
|
+
*/
|
|
42
|
+
export declare function pluginApi(id: PluginId, path: string, clusterId?: string): string;
|
|
43
|
+
/** The plugin's page, or the page explaining it is unavailable when the plugin is not running. */
|
|
44
|
+
export declare function pluginView(id: PluginId, View: ComponentType): () => ReactElement;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed-name confirmation for a destructive action (non-negotiable #2). The
|
|
3
|
+
* button arms only on an exact match of {@code token}. Extracted from the
|
|
4
|
+
* hand-rolled copies in `AddManagementUrl` (remove cluster) and `SettingsView`
|
|
5
|
+
* (credential rotation).
|
|
6
|
+
*/
|
|
7
|
+
export declare function ConfirmByTyping({ token, label, confirmLabel, loading, disabled, color, onConfirm, }: {
|
|
8
|
+
token: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
confirmLabel: string;
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
color?: string;
|
|
14
|
+
onConfirm: () => void;
|
|
15
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { components } from '../kernel/api/schema.d.ts';
|
|
2
|
+
type LifecycleOutcomeView = components['schemas']['LifecycleOutcomeView'];
|
|
3
|
+
/**
|
|
4
|
+
* One cluster-wide lifecycle result, rendered as a single object (ADR-0049 D2).
|
|
5
|
+
*
|
|
6
|
+
* <p>Used for both the dry-run preview and the result, so what the operator
|
|
7
|
+
* confirmed and what actually happened are visually comparable — a preview that
|
|
8
|
+
* looks nothing like its outcome makes the two impossible to check against each
|
|
9
|
+
* other.
|
|
10
|
+
*
|
|
11
|
+
* <p>Every node's state is carried in words. Colour is redundant with the text
|
|
12
|
+
* and appears only where something is wrong, so a fully applied command reads as
|
|
13
|
+
* near-monochrome and the eye stops at the one row that did not.
|
|
14
|
+
*/
|
|
15
|
+
export declare function NodeOutcomeSummary({ outcome, destructive, alreadyLabel, countNoun, verbFuture, verbPast, }: {
|
|
16
|
+
outcome: LifecycleOutcomeView;
|
|
17
|
+
/** A destroy shows the message counts; a pause has none worth a column. */
|
|
18
|
+
destructive?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* What `ALREADY` means for this command. It defaults to the lifecycle wording,
|
|
21
|
+
* but a connection close needs "already gone" — "already in this state" reads
|
|
22
|
+
* as though the operator had asked for something else.
|
|
23
|
+
*/
|
|
24
|
+
alreadyLabel?: string;
|
|
25
|
+
/**
|
|
26
|
+
* What the counts are counting, and the verb for them. Defaults to the queue
|
|
27
|
+
* delete's wording; a connection close affects consumers, not messages, and
|
|
28
|
+
* saying "destroyed" there would overstate what happened.
|
|
29
|
+
*/
|
|
30
|
+
countNoun?: string;
|
|
31
|
+
verbFuture?: string;
|
|
32
|
+
verbPast?: string;
|
|
33
|
+
}): import("react").JSX.Element;
|
|
34
|
+
/** One node's contribution, already in the words the caller's command uses. */
|
|
35
|
+
export interface OutcomeRow {
|
|
36
|
+
key: string;
|
|
37
|
+
name: string;
|
|
38
|
+
/** A right-aligned figure, when the command has one worth comparing between nodes. */
|
|
39
|
+
count?: string;
|
|
40
|
+
status: string;
|
|
41
|
+
tone?: 'warning' | 'danger';
|
|
42
|
+
/** A failure's reason, or anything else that needs a second line. */
|
|
43
|
+
detail?: string | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The per-node result as a shape, without an opinion about what the command was.
|
|
47
|
+
*
|
|
48
|
+
* <p>{@link NodeOutcomeSummary} is this with the lifecycle vocabulary; the SQL
|
|
49
|
+
* Console is this with a query's. Both go through here so a fan-out result reads
|
|
50
|
+
* the same wherever it appears — which is the point of the house rule, and is
|
|
51
|
+
* lost the moment a second screen re-implements the layout with its own spacing.
|
|
52
|
+
*/
|
|
53
|
+
export declare function OutcomeSummary({ verdict, verdictTone, total, rows, }: {
|
|
54
|
+
verdict: string;
|
|
55
|
+
verdictTone?: 'warning' | 'danger';
|
|
56
|
+
total?: string;
|
|
57
|
+
rows: OutcomeRow[];
|
|
58
|
+
}): import("react").JSX.Element;
|
|
59
|
+
/**
|
|
60
|
+
* Whether the command actually landed on every node it named — the one question a
|
|
61
|
+
* caller asks before treating the resource as changed.
|
|
62
|
+
*
|
|
63
|
+
* <p>Stated positively on purpose. `partial` is false both when everything worked
|
|
64
|
+
* and when nothing did (a cluster with no live node settles nowhere), so a caller
|
|
65
|
+
* that asks "not partial and nothing failed" concludes a delete succeeded against
|
|
66
|
+
* a cluster it never reached.
|
|
67
|
+
*/
|
|
68
|
+
export declare function appliedEverywhere(outcome: LifecycleOutcomeView): boolean;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Previous/Next over a server-side page, with the position stated.
|
|
3
|
+
*
|
|
4
|
+
* One implementation rather than one per view: a paged view that does not say
|
|
5
|
+
* where it is leaves the operator unable to tell whether they are looking at
|
|
6
|
+
* everything, which is exactly the failure ADR-0056 is about.
|
|
7
|
+
*
|
|
8
|
+
* Rendered even on a single page, because "1–14 of 14" is the sentence that
|
|
9
|
+
* settles the question; hiding it leaves the same doubt a missing pager does.
|
|
10
|
+
*/
|
|
11
|
+
export declare function Pager({ page, pageSize, total, onChange, label, }: {
|
|
12
|
+
/** 1-based. */
|
|
13
|
+
page: number;
|
|
14
|
+
pageSize: number;
|
|
15
|
+
total: number;
|
|
16
|
+
onChange: (page: number) => void;
|
|
17
|
+
/** Plural noun for the rows, e.g. `flows`. */
|
|
18
|
+
label: string;
|
|
19
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface GridColumn<T> {
|
|
2
|
+
id: string;
|
|
3
|
+
header: string;
|
|
4
|
+
accessor: (row: T) => unknown;
|
|
5
|
+
cell?: (row: T) => React.ReactNode;
|
|
6
|
+
numeric?: boolean;
|
|
7
|
+
/** The `sort` query value this column sorts by, if sortable. */
|
|
8
|
+
sortKey?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Fixed track width in px, for a column whose values have a known shape — a
|
|
11
|
+
* count, a routing type, a yes/no. Omit it for a column carrying free text
|
|
12
|
+
* (an address, a queue name, a client id): those share whatever space the
|
|
13
|
+
* fixed columns leave over, which is where a wide window is worth having.
|
|
14
|
+
*/
|
|
15
|
+
width?: number;
|
|
16
|
+
}
|
|
17
|
+
interface VirtualTableProps<T> {
|
|
18
|
+
columns: GridColumn<T>[];
|
|
19
|
+
data: T[];
|
|
20
|
+
sort?: string;
|
|
21
|
+
onSortChange?: (sort: string | undefined) => void;
|
|
22
|
+
onRowClick?: (row: T) => void;
|
|
23
|
+
rowKey: (row: T) => string;
|
|
24
|
+
emptyLabel?: React.ReactNode;
|
|
25
|
+
/** An extra class per row, for state the caller owns — a live tail's fresh rows. */
|
|
26
|
+
rowClassName?: (row: T) => string | undefined;
|
|
27
|
+
/** Opt-in leading checkbox column. Selection state is owned by the caller (ephemeral React state). */
|
|
28
|
+
selectable?: boolean;
|
|
29
|
+
selected?: ReadonlySet<string>;
|
|
30
|
+
onToggleRow?: (key: string) => void;
|
|
31
|
+
/** Header select-all across the loaded page. `allSelected` is the current state; the caller flips it. */
|
|
32
|
+
onToggleAll?: (keys: string[], allSelected: boolean) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Called when the grid's own scroll leaves or returns to the top. A live feed
|
|
35
|
+
* that prepends rows moves the content under a reader who has scrolled away, so
|
|
36
|
+
* the caller needs to know in order to hold new rows back.
|
|
37
|
+
*/
|
|
38
|
+
onAtTopChange?: (atTop: boolean) => void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A virtualized data grid: one CSS grid track list, declared once and shared by
|
|
42
|
+
* the header row and every body row, row-virtualized with
|
|
43
|
+
* `@tanstack/react-virtual`. Smooth at a few thousand rows.
|
|
44
|
+
*
|
|
45
|
+
* <p>Columns are either fixed or free. A fixed column gets exactly its declared
|
|
46
|
+
* px; a free column gets `minmax(floor, 1fr)`, so every pixel the window has
|
|
47
|
+
* over the fixed columns' needs goes to the values that actually vary in length.
|
|
48
|
+
* Below the floors the grid stops shrinking and its own container scrolls — the
|
|
49
|
+
* page never does.
|
|
50
|
+
*
|
|
51
|
+
* <p>Sorting is a URL round-trip, not local state: the header carries
|
|
52
|
+
* `aria-sort` from the current `sort` param and clicking it navigates. Row
|
|
53
|
+
* selection is opt-in (`selectable`) and its state lives with the caller.
|
|
54
|
+
*/
|
|
55
|
+
export declare function VirtualTable<T>({ columns, data, sort, onSortChange, onRowClick, rowKey, emptyLabel, rowClassName, selectable, selected, onToggleRow, onToggleAll, onAtTopChange, }: VirtualTableProps<T>): import("react").JSX.Element;
|
|
56
|
+
export {};
|
package/vite.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginOption } from 'vite';
|
|
2
|
+
|
|
3
|
+
/** Options for {@link studioPlugin}. */
|
|
4
|
+
export interface StudioPluginOptions {
|
|
5
|
+
/** The plugin's id, exactly as its `plugin.json` declares it. */
|
|
6
|
+
id: string;
|
|
7
|
+
/** The module whose default export is the plugin (`definePlugin(...)`). Default `./src/feature.tsx`. */
|
|
8
|
+
entry?: string;
|
|
9
|
+
/** Where the bundle goes. Default `target/classes/META-INF/artemis-studio/ui`, inside the plugin jar. */
|
|
10
|
+
outDir?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Vite configuration for a plugin's UI: a Module Federation remote that shares Studio's libraries. */
|
|
14
|
+
export function studioPlugin(options: StudioPluginOptions): PluginOption[];
|
package/vite.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { federation } from '@module-federation/vite';
|
|
2
|
+
|
|
3
|
+
import { SDK, SHARED_LIBRARIES, packageOf } from './shared.js';
|
|
4
|
+
import peers from './peers.json' with { type: 'json' };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Vite configuration for a plugin's UI (ADR-0100): a Module Federation remote named after the
|
|
8
|
+
* plugin, exposing `./feature`, that takes React, Mantine, TanStack and this SDK from the running
|
|
9
|
+
* Studio (`import: false`), with a relative base so it is served from wherever Studio mounts it,
|
|
10
|
+
* built into the jar's `META-INF/artemis-studio/ui/`.
|
|
11
|
+
*
|
|
12
|
+
* The build fails on an import Studio does not share — `@mantine/notifications`, say, or
|
|
13
|
+
* Mantine's CSS — since a plugin's own copy would load beside Studio's and silently misbehave.
|
|
14
|
+
*
|
|
15
|
+
* @param {{ id: string, entry?: string, outDir?: string }} options
|
|
16
|
+
* @returns {import('vite').PluginOption[]}
|
|
17
|
+
*/
|
|
18
|
+
export function studioPlugin({ id, entry = './src/feature.tsx', outDir = 'target/classes/META-INF/artemis-studio/ui' }) {
|
|
19
|
+
if (!/^[a-z][a-z0-9]*(-[a-z0-9]+)+$/.test(id)) {
|
|
20
|
+
throw new Error(`studioPlugin: "${id}" is not a plugin id (lowercase kebab-case, at least two segments, as in plugin.json)`);
|
|
21
|
+
}
|
|
22
|
+
const shared = Object.fromEntries([
|
|
23
|
+
...SHARED_LIBRARIES.map((name) => [
|
|
24
|
+
name,
|
|
25
|
+
{ singleton: true, import: false, requiredVersion: peers[packageOf(name)] },
|
|
26
|
+
]),
|
|
27
|
+
[SDK, { singleton: true, import: false, requiredVersion: false }],
|
|
28
|
+
]);
|
|
29
|
+
const allowed = new Set([...SHARED_LIBRARIES.map(packageOf), SDK]);
|
|
30
|
+
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
name: 'artemis-studio-plugin',
|
|
34
|
+
// A remote has no index.html: the exposed module is the build's input, remoteEntry.js its entry.
|
|
35
|
+
config: () => ({
|
|
36
|
+
base: './',
|
|
37
|
+
build: { outDir, emptyOutDir: true, target: 'esnext', rolldownOptions: { input: entry } },
|
|
38
|
+
}),
|
|
39
|
+
resolveId(source, importer) {
|
|
40
|
+
if (!importer || !source.startsWith('@mantine/')) return null;
|
|
41
|
+
if (allowed.has(packageOf(source)) && !source.endsWith('.css')) return null;
|
|
42
|
+
this.error(
|
|
43
|
+
`${source} is not shared by Studio, so a plugin would load its own copy beside Studio's. ` +
|
|
44
|
+
(source.startsWith('@mantine/notifications')
|
|
45
|
+
? 'Use notify from @artemis-studio/plugin-sdk instead.'
|
|
46
|
+
: source.endsWith('.css')
|
|
47
|
+
? "Studio already loads Mantine's styles; remove this import."
|
|
48
|
+
: 'Use @mantine/core and @mantine/hooks only.'),
|
|
49
|
+
);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
federation({
|
|
53
|
+
name: `plugin_${id.replace(/-/g, '_')}`,
|
|
54
|
+
filename: 'remoteEntry.js',
|
|
55
|
+
exposes: { './feature': entry },
|
|
56
|
+
shared,
|
|
57
|
+
dts: false,
|
|
58
|
+
}),
|
|
59
|
+
];
|
|
60
|
+
}
|