@stigmer/react 3.0.4 → 3.0.6
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/agent/AgentDetailView.d.ts +1 -11
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +3 -6
- package/agent/AgentDetailView.js.map +1 -1
- package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
- package/agent-instance/AgentInstanceDetailPanel.js +2 -6
- package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
- package/agent-instance/AgentInstanceList.d.ts.map +1 -1
- package/agent-instance/AgentInstanceList.js +2 -6
- package/agent-instance/AgentInstanceList.js.map +1 -1
- package/iam-policy/useCheckPermission.d.ts +3 -1
- package/iam-policy/useCheckPermission.d.ts.map +1 -1
- package/iam-policy/useCheckPermission.js +11 -15
- package/iam-policy/useCheckPermission.js.map +1 -1
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/library/InstanceVisibilitySelector.d.ts +8 -15
- package/library/InstanceVisibilitySelector.d.ts.map +1 -1
- package/library/InstanceVisibilitySelector.js +11 -139
- package/library/InstanceVisibilitySelector.js.map +1 -1
- package/library/ResourceVisibilityControl.d.ts +52 -0
- package/library/ResourceVisibilityControl.d.ts.map +1 -0
- package/library/ResourceVisibilityControl.js +81 -0
- package/library/ResourceVisibilityControl.js.map +1 -0
- package/library/ScopeToggle.d.ts +1 -1
- package/library/ScopeToggle.js +1 -1
- package/library/VisibilitySelector.d.ts +75 -0
- package/library/VisibilitySelector.d.ts.map +1 -0
- package/library/VisibilitySelector.js +171 -0
- package/library/VisibilitySelector.js.map +1 -0
- package/library/index.d.ts +6 -2
- package/library/index.d.ts.map +1 -1
- package/library/index.js +3 -1
- package/library/index.js.map +1 -1
- package/library/useUpdateVisibility.d.ts +5 -4
- package/library/useUpdateVisibility.d.ts.map +1 -1
- package/library/useUpdateVisibility.js +5 -4
- package/library/useUpdateVisibility.js.map +1 -1
- package/library/visibilityLevels.d.ts +74 -0
- package/library/visibilityLevels.d.ts.map +1 -0
- package/library/visibilityLevels.js +91 -0
- package/library/visibilityLevels.js.map +1 -0
- package/mcp-server/McpServerDetailView.d.ts +1 -11
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +3 -6
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/package.json +4 -4
- package/resource-detail/types.d.ts +1 -1
- package/skill/SkillDetailView.d.ts +1 -11
- package/skill/SkillDetailView.d.ts.map +1 -1
- package/skill/SkillDetailView.js +3 -6
- package/skill/SkillDetailView.js.map +1 -1
- package/src/agent/AgentDetailView.tsx +10 -35
- package/src/agent-instance/AgentInstanceDetailPanel.tsx +2 -7
- package/src/agent-instance/AgentInstanceList.tsx +2 -7
- package/src/iam-policy/useCheckPermission.ts +10 -15
- package/src/index.ts +8 -2
- package/src/library/InstanceVisibilitySelector.tsx +19 -276
- package/src/library/ResourceVisibilityControl.tsx +145 -0
- package/src/library/ScopeToggle.tsx +1 -1
- package/src/library/VisibilitySelector.tsx +393 -0
- package/src/library/index.ts +16 -2
- package/src/library/useUpdateVisibility.ts +5 -4
- package/src/library/visibilityLevels.ts +144 -0
- package/src/mcp-server/McpServerDetailView.tsx +10 -35
- package/src/resource-detail/types.ts +1 -1
- package/src/skill/SkillDetailView.tsx +10 -35
- package/src/workflow/WorkflowDetailView.tsx +10 -34
- package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +2 -7
- package/src/workflow/instance/WorkflowInstanceList.tsx +2 -7
- package/styles.css +1 -1
- package/workflow/WorkflowDetailView.d.ts +1 -10
- package/workflow/WorkflowDetailView.d.ts.map +1 -1
- package/workflow/WorkflowDetailView.js +3 -6
- package/workflow/WorkflowDetailView.js.map +1 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.js +2 -6
- package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
- package/workflow/instance/WorkflowInstanceList.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceList.js +2 -6
- package/workflow/instance/WorkflowInstanceList.js.map +1 -1
- package/library/VisibilityToggle.d.ts +0 -42
- package/library/VisibilityToggle.d.ts.map +0 -1
- package/library/VisibilityToggle.js +0 -89
- package/library/VisibilityToggle.js.map +0 -1
- package/src/library/VisibilityToggle.tsx +0 -247
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { cn } from "@stigmer/theme";
|
|
5
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
6
|
+
import {
|
|
7
|
+
visibilityOption,
|
|
8
|
+
type VisibilityLevelOption,
|
|
9
|
+
} from "./visibilityLevels";
|
|
10
|
+
|
|
11
|
+
/** Props for {@link VisibilitySelector}. */
|
|
12
|
+
export interface VisibilitySelectorProps {
|
|
13
|
+
/** Current visibility of the resource. */
|
|
14
|
+
readonly visibility: ApiResourceVisibility;
|
|
15
|
+
/**
|
|
16
|
+
* Levels to offer, in escalation order (see {@link blueprintVisibilityLevels}).
|
|
17
|
+
* Selecting a level later in the list than the current one is an
|
|
18
|
+
* escalation and shows that option's inline confirmation prompt;
|
|
19
|
+
* de-escalation applies immediately (revoking access is always safe).
|
|
20
|
+
*/
|
|
21
|
+
readonly options: readonly VisibilityLevelOption[];
|
|
22
|
+
/** Called when the user selects (and, for escalations, confirms) a level. */
|
|
23
|
+
readonly onVisibilityChange: (v: ApiResourceVisibility) => void;
|
|
24
|
+
/** Shows a spinner/disabled state while the RPC is in flight. */
|
|
25
|
+
readonly isPending?: boolean;
|
|
26
|
+
/** Disables all interaction (e.g., when the user lacks can_edit). */
|
|
27
|
+
readonly disabled?: boolean;
|
|
28
|
+
/** Accessible name for the radio group. Defaults to "Resource visibility". */
|
|
29
|
+
readonly ariaLabel?: string;
|
|
30
|
+
/** Additional CSS classes applied to the root element. */
|
|
31
|
+
readonly className?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Segmented visibility selector — the single control for resource
|
|
36
|
+
* visibility across blueprints AND instances. The offered levels are pure
|
|
37
|
+
* data ({@link VisibilityLevelOption}); per-kind level sets live in
|
|
38
|
+
* `visibilityLevels.ts`, so this component carries no kind-specific logic.
|
|
39
|
+
*
|
|
40
|
+
* Escalating (moving right in the options list) shows an inline
|
|
41
|
+
* confirmation prompt colored by the target level's tone, since expanding
|
|
42
|
+
* access is consequential. De-escalating applies immediately.
|
|
43
|
+
*
|
|
44
|
+
* If the current visibility is not among the offered options (e.g. a
|
|
45
|
+
* platform-shared blueprint whose org no longer operates an
|
|
46
|
+
* IdentityProvider), its canonical option is rendered in place so the
|
|
47
|
+
* state stays legible and the user can still move to an offered level.
|
|
48
|
+
*
|
|
49
|
+
* WAI-ARIA Radio Group with roving tabindex, following the same visual
|
|
50
|
+
* pattern as {@link ScopeToggle}. All visual properties flow through
|
|
51
|
+
* `--stgm-*` design tokens.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```tsx
|
|
55
|
+
* <VisibilitySelector
|
|
56
|
+
* visibility={agent.metadata.visibility}
|
|
57
|
+
* options={blueprintVisibilityLevels({ deploymentMode, hasIdentityProvider })}
|
|
58
|
+
* onVisibilityChange={updateVisibility}
|
|
59
|
+
* isPending={isPending}
|
|
60
|
+
* />
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function VisibilitySelector({
|
|
64
|
+
visibility,
|
|
65
|
+
options,
|
|
66
|
+
onVisibilityChange,
|
|
67
|
+
isPending = false,
|
|
68
|
+
disabled = false,
|
|
69
|
+
ariaLabel = "Resource visibility",
|
|
70
|
+
className,
|
|
71
|
+
}: VisibilitySelectorProps) {
|
|
72
|
+
const [confirming, setConfirming] = useState<ApiResourceVisibility | null>(
|
|
73
|
+
null,
|
|
74
|
+
);
|
|
75
|
+
const optionRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
76
|
+
const effectivelyDisabled = disabled || isPending;
|
|
77
|
+
|
|
78
|
+
// Keep the current state legible even when it is not offerable in the
|
|
79
|
+
// current context: render its canonical option as an extra segment.
|
|
80
|
+
const effectiveOptions = useMemo(() => {
|
|
81
|
+
if (options.some((o) => o.value === visibility)) return options;
|
|
82
|
+
return [...options, visibilityOption(visibility)];
|
|
83
|
+
}, [options, visibility]);
|
|
84
|
+
|
|
85
|
+
const isEscalation = useCallback(
|
|
86
|
+
(target: ApiResourceVisibility) => {
|
|
87
|
+
const values = effectiveOptions.map((o) => o.value);
|
|
88
|
+
return values.indexOf(target) > values.indexOf(visibility);
|
|
89
|
+
},
|
|
90
|
+
[effectiveOptions, visibility],
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const handleSelect = useCallback(
|
|
94
|
+
(value: ApiResourceVisibility) => {
|
|
95
|
+
if (value === visibility) return;
|
|
96
|
+
|
|
97
|
+
if (isEscalation(value)) {
|
|
98
|
+
setConfirming(value);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
setConfirming(null);
|
|
103
|
+
onVisibilityChange(value);
|
|
104
|
+
},
|
|
105
|
+
[visibility, onVisibilityChange, isEscalation],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const confirmChange = useCallback(() => {
|
|
109
|
+
if (confirming === null) return;
|
|
110
|
+
setConfirming(null);
|
|
111
|
+
onVisibilityChange(confirming);
|
|
112
|
+
}, [confirming, onVisibilityChange]);
|
|
113
|
+
|
|
114
|
+
const cancelConfirm = useCallback(() => {
|
|
115
|
+
setConfirming(null);
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
118
|
+
const handleKeyDown = useCallback(
|
|
119
|
+
(e: React.KeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
120
|
+
let nextIndex: number | null = null;
|
|
121
|
+
|
|
122
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
nextIndex = (index + 1) % effectiveOptions.length;
|
|
125
|
+
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
|
126
|
+
e.preventDefault();
|
|
127
|
+
nextIndex =
|
|
128
|
+
(index - 1 + effectiveOptions.length) % effectiveOptions.length;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (nextIndex !== null) {
|
|
132
|
+
optionRefs.current[nextIndex]?.focus();
|
|
133
|
+
handleSelect(effectiveOptions[nextIndex].value);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
[effectiveOptions, handleSelect],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const confirmingOption =
|
|
140
|
+
confirming !== null
|
|
141
|
+
? effectiveOptions.find((o) => o.value === confirming)
|
|
142
|
+
: undefined;
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className={cn("inline-flex flex-col gap-1.5", className)}>
|
|
146
|
+
<div
|
|
147
|
+
role="radiogroup"
|
|
148
|
+
aria-label={ariaLabel}
|
|
149
|
+
aria-disabled={effectivelyDisabled || undefined}
|
|
150
|
+
className={cn(
|
|
151
|
+
"inline-flex rounded-md bg-muted p-0.5",
|
|
152
|
+
effectivelyDisabled && "pointer-events-none opacity-50",
|
|
153
|
+
)}
|
|
154
|
+
>
|
|
155
|
+
{effectiveOptions.map((option, index) => {
|
|
156
|
+
const isSelected = visibility === option.value;
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<button
|
|
160
|
+
key={option.value}
|
|
161
|
+
ref={(el) => {
|
|
162
|
+
optionRefs.current[index] = el;
|
|
163
|
+
}}
|
|
164
|
+
type="button"
|
|
165
|
+
role="radio"
|
|
166
|
+
aria-checked={isSelected}
|
|
167
|
+
aria-label={`${option.label}: ${option.description}`}
|
|
168
|
+
tabIndex={isSelected ? 0 : -1}
|
|
169
|
+
disabled={effectivelyDisabled}
|
|
170
|
+
onClick={() => handleSelect(option.value)}
|
|
171
|
+
onKeyDown={(e) => handleKeyDown(e, index)}
|
|
172
|
+
className={cn(
|
|
173
|
+
"inline-flex cursor-pointer items-center gap-1 rounded-sm px-2.5 py-1 text-xs font-medium transition-colors",
|
|
174
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
175
|
+
isSelected
|
|
176
|
+
? SELECTED_STYLES[option.tone]
|
|
177
|
+
: "text-muted-foreground hover:text-foreground",
|
|
178
|
+
)}
|
|
179
|
+
>
|
|
180
|
+
{isPending && isSelected ? (
|
|
181
|
+
<span
|
|
182
|
+
className="inline-block size-3 animate-spin rounded-full border-2 border-current border-t-transparent"
|
|
183
|
+
aria-hidden="true"
|
|
184
|
+
/>
|
|
185
|
+
) : (
|
|
186
|
+
<VisibilityIcon tone={option.tone} className="size-3" />
|
|
187
|
+
)}
|
|
188
|
+
{option.label}
|
|
189
|
+
</button>
|
|
190
|
+
);
|
|
191
|
+
})}
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{/* Description of current state */}
|
|
195
|
+
{confirming === null && (
|
|
196
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
197
|
+
{effectiveOptions.find((o) => o.value === visibility)?.description}
|
|
198
|
+
</p>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
{/* Confirmation prompt for escalation */}
|
|
202
|
+
{confirmingOption?.confirmPrompt && (
|
|
203
|
+
<div
|
|
204
|
+
className={cn(
|
|
205
|
+
"flex items-center gap-2 rounded-md border px-3 py-1.5 text-xs",
|
|
206
|
+
PROMPT_STYLES[confirmingOption.tone].container,
|
|
207
|
+
)}
|
|
208
|
+
role="alert"
|
|
209
|
+
>
|
|
210
|
+
<span className={PROMPT_STYLES[confirmingOption.tone].text}>
|
|
211
|
+
{confirmingOption.confirmPrompt}
|
|
212
|
+
</span>
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
onClick={confirmChange}
|
|
216
|
+
className={cn(
|
|
217
|
+
"rounded px-2 py-0.5 text-xs font-medium text-white",
|
|
218
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
219
|
+
PROMPT_STYLES[confirmingOption.tone].confirm,
|
|
220
|
+
)}
|
|
221
|
+
>
|
|
222
|
+
Confirm
|
|
223
|
+
</button>
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
onClick={cancelConfirm}
|
|
227
|
+
className={cn(
|
|
228
|
+
"rounded px-2 py-0.5 text-xs font-medium",
|
|
229
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
230
|
+
PROMPT_STYLES[confirmingOption.tone].cancel,
|
|
231
|
+
)}
|
|
232
|
+
>
|
|
233
|
+
Cancel
|
|
234
|
+
</button>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Tone styling — one row per visibility tone, keyed by VisibilityLevelOption
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
type Tone = VisibilityLevelOption["tone"];
|
|
246
|
+
|
|
247
|
+
const SELECTED_STYLES: Record<Tone, string> = {
|
|
248
|
+
private:
|
|
249
|
+
"bg-amber-50 text-amber-800 shadow-sm dark:bg-amber-900/30 dark:text-amber-300",
|
|
250
|
+
org: "bg-blue-100 text-blue-800 shadow-sm dark:bg-blue-900/40 dark:text-blue-300",
|
|
251
|
+
platform:
|
|
252
|
+
"bg-violet-100 text-violet-800 shadow-sm dark:bg-violet-900/40 dark:text-violet-300",
|
|
253
|
+
public:
|
|
254
|
+
"bg-emerald-100 text-emerald-800 shadow-sm dark:bg-emerald-900/40 dark:text-emerald-300",
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const PROMPT_STYLES: Record<
|
|
258
|
+
Tone,
|
|
259
|
+
{ container: string; text: string; confirm: string; cancel: string }
|
|
260
|
+
> = {
|
|
261
|
+
// Private never escalates, but the row keeps the Record total.
|
|
262
|
+
private: {
|
|
263
|
+
container:
|
|
264
|
+
"border-amber-200 bg-amber-50 dark:border-amber-800/50 dark:bg-amber-950/30",
|
|
265
|
+
text: "text-amber-800 dark:text-amber-200",
|
|
266
|
+
confirm:
|
|
267
|
+
"bg-amber-600 hover:bg-amber-700 dark:bg-amber-600 dark:hover:bg-amber-500",
|
|
268
|
+
cancel:
|
|
269
|
+
"text-amber-700 hover:text-amber-900 dark:text-amber-300 dark:hover:text-amber-100",
|
|
270
|
+
},
|
|
271
|
+
org: {
|
|
272
|
+
container:
|
|
273
|
+
"border-blue-200 bg-blue-50 dark:border-blue-800/50 dark:bg-blue-950/30",
|
|
274
|
+
text: "text-blue-800 dark:text-blue-200",
|
|
275
|
+
confirm:
|
|
276
|
+
"bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-500",
|
|
277
|
+
cancel:
|
|
278
|
+
"text-blue-700 hover:text-blue-900 dark:text-blue-300 dark:hover:text-blue-100",
|
|
279
|
+
},
|
|
280
|
+
platform: {
|
|
281
|
+
container:
|
|
282
|
+
"border-violet-200 bg-violet-50 dark:border-violet-800/50 dark:bg-violet-950/30",
|
|
283
|
+
text: "text-violet-800 dark:text-violet-200",
|
|
284
|
+
confirm:
|
|
285
|
+
"bg-violet-600 hover:bg-violet-700 dark:bg-violet-600 dark:hover:bg-violet-500",
|
|
286
|
+
cancel:
|
|
287
|
+
"text-violet-700 hover:text-violet-900 dark:text-violet-300 dark:hover:text-violet-100",
|
|
288
|
+
},
|
|
289
|
+
public: {
|
|
290
|
+
container:
|
|
291
|
+
"border-amber-200 bg-amber-50 dark:border-amber-800/50 dark:bg-amber-950/30",
|
|
292
|
+
text: "text-amber-800 dark:text-amber-200",
|
|
293
|
+
confirm:
|
|
294
|
+
"bg-amber-600 hover:bg-amber-700 dark:bg-amber-600 dark:hover:bg-amber-500",
|
|
295
|
+
cancel:
|
|
296
|
+
"text-amber-700 hover:text-amber-900 dark:text-amber-300 dark:hover:text-amber-100",
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Icons — inline SVGs following the SDK pattern (no icon library dependency)
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
/** Icon for a visibility tone; shared with {@link VisibilityBadge}. */
|
|
305
|
+
export function VisibilityIcon({
|
|
306
|
+
tone,
|
|
307
|
+
className,
|
|
308
|
+
}: {
|
|
309
|
+
readonly tone: Tone;
|
|
310
|
+
readonly className?: string;
|
|
311
|
+
}) {
|
|
312
|
+
switch (tone) {
|
|
313
|
+
case "org":
|
|
314
|
+
return <UsersIcon className={className} />;
|
|
315
|
+
case "platform":
|
|
316
|
+
return <BuildingsIcon className={className} />;
|
|
317
|
+
case "public":
|
|
318
|
+
return <GlobeIcon className={className} />;
|
|
319
|
+
default:
|
|
320
|
+
return <LockIcon className={className} />;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function LockIcon({ className }: { readonly className?: string }) {
|
|
325
|
+
return (
|
|
326
|
+
<svg className={className} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
327
|
+
<rect x="3.5" y="7" width="9" height="7" rx="1.5" />
|
|
328
|
+
<path d="M5.5 7V5a2.5 2.5 0 0 1 5 0v2" />
|
|
329
|
+
</svg>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function UsersIcon({ className }: { readonly className?: string }) {
|
|
334
|
+
return (
|
|
335
|
+
<svg className={className} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
336
|
+
<circle cx="6" cy="5" r="2.5" />
|
|
337
|
+
<path d="M2 13c0-2.21 1.79-4 4-4s4 1.79 4 4" />
|
|
338
|
+
<circle cx="11.5" cy="5.5" r="2" />
|
|
339
|
+
<path d="M14 13c0-1.66-1.12-3-2.5-3-.5 0-1 .14-1.4.4" />
|
|
340
|
+
</svg>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function BuildingsIcon({ className }: { readonly className?: string }) {
|
|
345
|
+
return (
|
|
346
|
+
<svg className={className} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
347
|
+
<path d="M2 14V4.5L6.5 2v12" />
|
|
348
|
+
<path d="M6.5 6.5 14 8.5V14" />
|
|
349
|
+
<path d="M2 14h12" />
|
|
350
|
+
<path d="M4.25 6h.01M4.25 8.5h.01M4.25 11h.01M10.5 10.5h.01M10.5 12.5h.01" />
|
|
351
|
+
</svg>
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function GlobeIcon({ className }: { readonly className?: string }) {
|
|
356
|
+
return (
|
|
357
|
+
<svg className={className} viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
358
|
+
<circle cx="8" cy="8" r="6" />
|
|
359
|
+
<path d="M2 8h12" />
|
|
360
|
+
<path d="M8 2c1.66 1.46 2.6 3.63 2.6 6s-.94 4.54-2.6 6c-1.66-1.46-2.6-3.63-2.6-6s.94-4.54 2.6-6Z" />
|
|
361
|
+
</svg>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Read-only visibility indicator with a matching icon, covering all four
|
|
367
|
+
* levels (Private / Organization / Platform / Public).
|
|
368
|
+
*
|
|
369
|
+
* Rendered wherever the interactive {@link VisibilitySelector} is not
|
|
370
|
+
* available — for viewers who lack `can_edit`, and while a permission check
|
|
371
|
+
* is in flight — so a resource's visibility is always legible rather than
|
|
372
|
+
* silently blank.
|
|
373
|
+
*/
|
|
374
|
+
export function VisibilityBadge({
|
|
375
|
+
visibility,
|
|
376
|
+
className,
|
|
377
|
+
}: {
|
|
378
|
+
readonly visibility: ApiResourceVisibility;
|
|
379
|
+
readonly className?: string;
|
|
380
|
+
}) {
|
|
381
|
+
const option = visibilityOption(visibility);
|
|
382
|
+
return (
|
|
383
|
+
<span
|
|
384
|
+
className={cn(
|
|
385
|
+
"inline-flex shrink-0 items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground",
|
|
386
|
+
className,
|
|
387
|
+
)}
|
|
388
|
+
>
|
|
389
|
+
<VisibilityIcon tone={option.tone} className="size-2.5" />
|
|
390
|
+
{option.label}
|
|
391
|
+
</span>
|
|
392
|
+
);
|
|
393
|
+
}
|
package/src/library/index.ts
CHANGED
|
@@ -68,8 +68,22 @@ export type {
|
|
|
68
68
|
export { ImportResourceDialog } from "./ImportResourceDialog";
|
|
69
69
|
export type { ImportResourceDialogProps } from "./ImportResourceDialog";
|
|
70
70
|
|
|
71
|
-
export {
|
|
72
|
-
export type {
|
|
71
|
+
export { VisibilitySelector, VisibilityBadge } from "./VisibilitySelector";
|
|
72
|
+
export type { VisibilitySelectorProps } from "./VisibilitySelector";
|
|
73
|
+
|
|
74
|
+
export {
|
|
75
|
+
blueprintVisibilityLevels,
|
|
76
|
+
INSTANCE_VISIBILITY_LEVELS,
|
|
77
|
+
visibilityLabel,
|
|
78
|
+
visibilityOption,
|
|
79
|
+
} from "./visibilityLevels";
|
|
80
|
+
export type {
|
|
81
|
+
BlueprintVisibilityLevelsContext,
|
|
82
|
+
VisibilityLevelOption,
|
|
83
|
+
} from "./visibilityLevels";
|
|
84
|
+
|
|
85
|
+
export { ResourceVisibilityControl } from "./ResourceVisibilityControl";
|
|
86
|
+
export type { ResourceVisibilityControlProps } from "./ResourceVisibilityControl";
|
|
73
87
|
|
|
74
88
|
export { useUpdateVisibility } from "./useUpdateVisibility";
|
|
75
89
|
export type {
|
|
@@ -34,9 +34,9 @@ export interface UseUpdateVisibilityReturn {
|
|
|
34
34
|
/**
|
|
35
35
|
* Behavior hook that updates the visibility of a resource.
|
|
36
36
|
*
|
|
37
|
-
* Supports blueprints (Agent, Workflow, Skill, MCP Server) with
|
|
38
|
-
* private/public
|
|
39
|
-
* WorkflowInstance) with
|
|
37
|
+
* Supports blueprints (Agent, Workflow, Skill, MCP Server) with the
|
|
38
|
+
* full private/org/public/platform spectrum, and instances
|
|
39
|
+
* (AgentInstance, WorkflowInstance) with private/org/public.
|
|
40
40
|
*
|
|
41
41
|
* Wraps the generated `stigmer.{kind}.updateVisibility()` SDK method
|
|
42
42
|
* with loading and error state management. The hook is stateless with
|
|
@@ -51,8 +51,9 @@ export interface UseUpdateVisibilityReturn {
|
|
|
51
51
|
* ```tsx
|
|
52
52
|
* const { updateVisibility, isPending } = useUpdateVisibility("workflow", workflow.metadata.id);
|
|
53
53
|
*
|
|
54
|
-
* <
|
|
54
|
+
* <VisibilitySelector
|
|
55
55
|
* visibility={workflow.metadata.visibility}
|
|
56
|
+
* options={options}
|
|
56
57
|
* onVisibilityChange={updateVisibility}
|
|
57
58
|
* isPending={isPending}
|
|
58
59
|
* />
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A selectable visibility level: label, explanation, and escalation copy.
|
|
5
|
+
*
|
|
6
|
+
* Options are always declared in escalation order (least to most exposed:
|
|
7
|
+
* private < org < platform < public); {@link VisibilitySelector} derives
|
|
8
|
+
* "is this an escalation?" from array position, and shows
|
|
9
|
+
* {@link confirmPrompt} before applying one.
|
|
10
|
+
*/
|
|
11
|
+
export interface VisibilityLevelOption {
|
|
12
|
+
readonly value: ApiResourceVisibility;
|
|
13
|
+
readonly label: string;
|
|
14
|
+
/** One-line explanation shown under the selector for the current level. */
|
|
15
|
+
readonly description: string;
|
|
16
|
+
/**
|
|
17
|
+
* Inline confirmation question shown when the user escalates TO this
|
|
18
|
+
* level. Omitted for the least-exposed level (de-escalation never
|
|
19
|
+
* confirms — revoking access is always safe).
|
|
20
|
+
*/
|
|
21
|
+
readonly confirmPrompt?: string;
|
|
22
|
+
/** Color treatment for the selected segment and the confirmation prompt. */
|
|
23
|
+
readonly tone: "private" | "org" | "platform" | "public";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const PRIVATE_OPTION: VisibilityLevelOption = {
|
|
27
|
+
value: ApiResourceVisibility.visibility_private,
|
|
28
|
+
label: "Private",
|
|
29
|
+
description: "Only you can access",
|
|
30
|
+
tone: "private",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const ORG_OPTION: VisibilityLevelOption = {
|
|
34
|
+
value: ApiResourceVisibility.visibility_org,
|
|
35
|
+
label: "Organization",
|
|
36
|
+
description: "All members of your organization",
|
|
37
|
+
confirmPrompt: "Make visible to all org members?",
|
|
38
|
+
tone: "org",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const PLATFORM_OPTION: VisibilityLevelOption = {
|
|
42
|
+
value: ApiResourceVisibility.visibility_platform,
|
|
43
|
+
label: "Platform",
|
|
44
|
+
description: "All organizations managed by your platform",
|
|
45
|
+
confirmPrompt: "Share with every organization managed by your platform?",
|
|
46
|
+
tone: "platform",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const PUBLIC_OPTION: VisibilityLevelOption = {
|
|
50
|
+
value: ApiResourceVisibility.visibility_public,
|
|
51
|
+
label: "Public",
|
|
52
|
+
description: "Anyone on Stigmer",
|
|
53
|
+
confirmPrompt: "Make visible to all authenticated users?",
|
|
54
|
+
tone: "public",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Inputs that gate which levels a blueprint selector offers.
|
|
59
|
+
*
|
|
60
|
+
* Mirrors the backend's per-kind `VisibilityConfig` plus runtime context the
|
|
61
|
+
* proto cannot know:
|
|
62
|
+
*
|
|
63
|
+
* - `deploymentMode`: the OSS Go backend (`local`) is single-user and
|
|
64
|
+
* performs no org/platform visibility gating, so only Private/Public are
|
|
65
|
+
* meaningful there.
|
|
66
|
+
* - `hasIdentityProvider`: `visibility_platform` requires the owning org to
|
|
67
|
+
* operate an IdentityProvider — the backend rejects it otherwise
|
|
68
|
+
* (`ValidateVisibilityStep`), so the option only renders when the signal
|
|
69
|
+
* is present (use `useSsoProvider`, the permission-free lookup).
|
|
70
|
+
*/
|
|
71
|
+
export interface BlueprintVisibilityLevelsContext {
|
|
72
|
+
readonly deploymentMode: "cloud" | "local";
|
|
73
|
+
readonly hasIdentityProvider: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The levels a blueprint (agent, skill, workflow, mcp_server) selector
|
|
78
|
+
* offers, in escalation order.
|
|
79
|
+
*
|
|
80
|
+
* Cloud: Private / Organization [/ Platform] / Public — Organization is the
|
|
81
|
+
* creation default (blueprints are shared org assets; Private is an explicit
|
|
82
|
+
* opt-in). Local: Private / Public.
|
|
83
|
+
*/
|
|
84
|
+
export function blueprintVisibilityLevels(
|
|
85
|
+
context: BlueprintVisibilityLevelsContext,
|
|
86
|
+
): readonly VisibilityLevelOption[] {
|
|
87
|
+
if (context.deploymentMode === "local") {
|
|
88
|
+
return [PRIVATE_OPTION, PUBLIC_OPTION];
|
|
89
|
+
}
|
|
90
|
+
return context.hasIdentityProvider
|
|
91
|
+
? [PRIVATE_OPTION, ORG_OPTION, PLATFORM_OPTION, PUBLIC_OPTION]
|
|
92
|
+
: [PRIVATE_OPTION, ORG_OPTION, PUBLIC_OPTION];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The levels an instance (agent_instance, workflow_instance) selector
|
|
97
|
+
* offers, in escalation order: Private / Organization / Public.
|
|
98
|
+
*
|
|
99
|
+
* Platform is deliberately absent — instances are tenant-isolated by
|
|
100
|
+
* design (each managed org instantiates shared blueprints inside its own
|
|
101
|
+
* boundary). Descriptions are execution-oriented because org visibility on
|
|
102
|
+
* an instance is about who can run it and see its executions.
|
|
103
|
+
*/
|
|
104
|
+
export const INSTANCE_VISIBILITY_LEVELS: readonly VisibilityLevelOption[] = [
|
|
105
|
+
PRIVATE_OPTION,
|
|
106
|
+
{
|
|
107
|
+
...ORG_OPTION,
|
|
108
|
+
description: "All org members can view executions",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
...PUBLIC_OPTION,
|
|
112
|
+
description: "All authenticated users can view",
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Canonical option for a visibility value, independent of any kind's offered
|
|
118
|
+
* list. Used to render the current level even when it is not offerable in
|
|
119
|
+
* the current context (e.g. a platform-shared blueprint whose org no longer
|
|
120
|
+
* operates an IdentityProvider) — the state must stay legible.
|
|
121
|
+
*/
|
|
122
|
+
export function visibilityOption(
|
|
123
|
+
visibility: ApiResourceVisibility,
|
|
124
|
+
): VisibilityLevelOption {
|
|
125
|
+
switch (visibility) {
|
|
126
|
+
case ApiResourceVisibility.visibility_org:
|
|
127
|
+
return ORG_OPTION;
|
|
128
|
+
case ApiResourceVisibility.visibility_platform:
|
|
129
|
+
return PLATFORM_OPTION;
|
|
130
|
+
case ApiResourceVisibility.visibility_public:
|
|
131
|
+
return PUBLIC_OPTION;
|
|
132
|
+
default:
|
|
133
|
+
return PRIVATE_OPTION;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Human label for a visibility value — the one place list rows, badges, and
|
|
139
|
+
* detail panels resolve enum-to-text, so no surface ever falls through to
|
|
140
|
+
* "Private" (or "unknown") for org/platform.
|
|
141
|
+
*/
|
|
142
|
+
export function visibilityLabel(visibility: ApiResourceVisibility): string {
|
|
143
|
+
return visibilityOption(visibility).label;
|
|
144
|
+
}
|
|
@@ -13,7 +13,6 @@ import type {
|
|
|
13
13
|
import type { ToolApprovalPolicy, McpServerSpec } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
|
|
14
14
|
import { ValidationState } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
|
|
15
15
|
import type { EnvVarDeclaration } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
|
|
16
|
-
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
17
16
|
import { useMcpServer } from "./useMcpServer";
|
|
18
17
|
import { useUpdateMcpServer } from "./useUpdateMcpServer";
|
|
19
18
|
import { mcpServerToInput } from "./internal/mcpServerToInput";
|
|
@@ -27,8 +26,7 @@ import { OAuthAppForm } from "./OAuthAppForm";
|
|
|
27
26
|
import { ErrorMessage } from "../error/ErrorMessage";
|
|
28
27
|
import { EnvVarForm } from "../environment/EnvVarForm";
|
|
29
28
|
import type { EnvVarFormVariable } from "../environment/EnvVarForm";
|
|
30
|
-
import {
|
|
31
|
-
import { PermissionGate } from "../iam-policy/PermissionGate";
|
|
29
|
+
import { ResourceVisibilityControl } from "../library/ResourceVisibilityControl";
|
|
32
30
|
import { Tabs, type TabItem } from "../tabs/Tabs";
|
|
33
31
|
import { ResourceDetailShell } from "../resource-detail/ResourceDetailShell";
|
|
34
32
|
import { Section } from "../resource-detail/Section";
|
|
@@ -58,15 +56,6 @@ export interface McpServerDetailViewProps {
|
|
|
58
56
|
* Not called on error or not-found states.
|
|
59
57
|
*/
|
|
60
58
|
readonly onResourceLoad?: (meta: { name: string; id: string }) => void;
|
|
61
|
-
/**
|
|
62
|
-
* Called when the user toggles visibility via the inline control.
|
|
63
|
-
* When provided, the header renders an interactive
|
|
64
|
-
* {@link VisibilityToggle} instead of a read-only badge.
|
|
65
|
-
* When omitted, visibility is displayed as a static "Public" pill.
|
|
66
|
-
*/
|
|
67
|
-
readonly onVisibilityChange?: (v: ApiResourceVisibility) => void;
|
|
68
|
-
/** `true` while a visibility update RPC is in flight. */
|
|
69
|
-
readonly isVisibilityPending?: boolean;
|
|
70
59
|
/**
|
|
71
60
|
* Initial active capability tab. Defaults to `"tools"`.
|
|
72
61
|
* Useful for deep-linking or demo scenarios that need to start on
|
|
@@ -147,8 +136,6 @@ export function McpServerDetailView({
|
|
|
147
136
|
org,
|
|
148
137
|
slug,
|
|
149
138
|
onResourceLoad,
|
|
150
|
-
onVisibilityChange,
|
|
151
|
-
isVisibilityPending,
|
|
152
139
|
defaultCapabilityTab = "tools",
|
|
153
140
|
defaultShowCredentialForm = false,
|
|
154
141
|
credentialPoolValues,
|
|
@@ -393,27 +380,15 @@ export function McpServerDetailView({
|
|
|
393
380
|
updatedAt: specAudit?.updatedAt ? timestampDate(specAudit.updatedAt) : null,
|
|
394
381
|
};
|
|
395
382
|
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
<PermissionGate
|
|
406
|
-
resource={{ kind: "mcp_server", id: meta.id }}
|
|
407
|
-
relation="can_edit"
|
|
408
|
-
fallback={visibilityBadge}
|
|
409
|
-
>
|
|
410
|
-
<VisibilityToggle
|
|
411
|
-
visibility={meta.visibility}
|
|
412
|
-
onVisibilityChange={onVisibilityChange}
|
|
413
|
-
isPending={isVisibilityPending}
|
|
414
|
-
/>
|
|
415
|
-
</PermissionGate>
|
|
416
|
-
) : visibilityBadge;
|
|
383
|
+
const visibilityControl = meta ? (
|
|
384
|
+
<ResourceVisibilityControl
|
|
385
|
+
kind="mcpServer"
|
|
386
|
+
resourceId={meta.id}
|
|
387
|
+
visibility={meta.visibility}
|
|
388
|
+
org={meta.org || org}
|
|
389
|
+
onChanged={refetch}
|
|
390
|
+
/>
|
|
391
|
+
) : undefined;
|
|
417
392
|
|
|
418
393
|
const headerMetaExtra = (
|
|
419
394
|
<>
|
|
@@ -151,7 +151,7 @@ export interface ResourceDetailShellProps {
|
|
|
151
151
|
|
|
152
152
|
/**
|
|
153
153
|
* Visibility control rendered in the header.
|
|
154
|
-
* Typically a `<
|
|
154
|
+
* Typically a `<ResourceVisibilityControl />` from the library module.
|
|
155
155
|
* Rendered inline after the resource name.
|
|
156
156
|
*/
|
|
157
157
|
readonly visibilityControl?: ReactNode;
|