@stigmer/react 3.0.5 → 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.
Files changed (80) hide show
  1. package/agent/AgentDetailView.d.ts.map +1 -1
  2. package/agent/AgentDetailView.js +1 -1
  3. package/agent/AgentDetailView.js.map +1 -1
  4. package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
  5. package/agent-instance/AgentInstanceDetailPanel.js +2 -6
  6. package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
  7. package/agent-instance/AgentInstanceList.d.ts.map +1 -1
  8. package/agent-instance/AgentInstanceList.js +2 -6
  9. package/agent-instance/AgentInstanceList.js.map +1 -1
  10. package/index.d.ts +2 -2
  11. package/index.d.ts.map +1 -1
  12. package/index.js +1 -1
  13. package/index.js.map +1 -1
  14. package/library/InstanceVisibilitySelector.d.ts +8 -15
  15. package/library/InstanceVisibilitySelector.d.ts.map +1 -1
  16. package/library/InstanceVisibilitySelector.js +11 -139
  17. package/library/InstanceVisibilitySelector.js.map +1 -1
  18. package/library/ResourceVisibilityControl.d.ts +23 -6
  19. package/library/ResourceVisibilityControl.d.ts.map +1 -1
  20. package/library/ResourceVisibilityControl.js +38 -9
  21. package/library/ResourceVisibilityControl.js.map +1 -1
  22. package/library/ScopeToggle.d.ts +1 -1
  23. package/library/ScopeToggle.js +1 -1
  24. package/library/VisibilitySelector.d.ts +75 -0
  25. package/library/VisibilitySelector.d.ts.map +1 -0
  26. package/library/VisibilitySelector.js +171 -0
  27. package/library/VisibilitySelector.js.map +1 -0
  28. package/library/index.d.ts +4 -2
  29. package/library/index.d.ts.map +1 -1
  30. package/library/index.js +2 -1
  31. package/library/index.js.map +1 -1
  32. package/library/useUpdateVisibility.d.ts +5 -4
  33. package/library/useUpdateVisibility.d.ts.map +1 -1
  34. package/library/useUpdateVisibility.js +5 -4
  35. package/library/useUpdateVisibility.js.map +1 -1
  36. package/library/visibilityLevels.d.ts +74 -0
  37. package/library/visibilityLevels.d.ts.map +1 -0
  38. package/library/visibilityLevels.js +91 -0
  39. package/library/visibilityLevels.js.map +1 -0
  40. package/mcp-server/McpServerDetailView.d.ts +1 -11
  41. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  42. package/mcp-server/McpServerDetailView.js +3 -6
  43. package/mcp-server/McpServerDetailView.js.map +1 -1
  44. package/package.json +4 -4
  45. package/resource-detail/types.d.ts +1 -1
  46. package/skill/SkillDetailView.d.ts.map +1 -1
  47. package/skill/SkillDetailView.js +1 -1
  48. package/skill/SkillDetailView.js.map +1 -1
  49. package/src/agent/AgentDetailView.tsx +1 -0
  50. package/src/agent-instance/AgentInstanceDetailPanel.tsx +2 -7
  51. package/src/agent-instance/AgentInstanceList.tsx +2 -7
  52. package/src/index.ts +8 -2
  53. package/src/library/InstanceVisibilitySelector.tsx +19 -276
  54. package/src/library/ResourceVisibilityControl.tsx +54 -8
  55. package/src/library/ScopeToggle.tsx +1 -1
  56. package/src/library/VisibilitySelector.tsx +393 -0
  57. package/src/library/index.ts +13 -2
  58. package/src/library/useUpdateVisibility.ts +5 -4
  59. package/src/library/visibilityLevels.ts +144 -0
  60. package/src/mcp-server/McpServerDetailView.tsx +10 -35
  61. package/src/resource-detail/types.ts +1 -1
  62. package/src/skill/SkillDetailView.tsx +1 -0
  63. package/src/workflow/WorkflowDetailView.tsx +1 -0
  64. package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +2 -7
  65. package/src/workflow/instance/WorkflowInstanceList.tsx +2 -7
  66. package/styles.css +1 -1
  67. package/workflow/WorkflowDetailView.d.ts.map +1 -1
  68. package/workflow/WorkflowDetailView.js +1 -1
  69. package/workflow/WorkflowDetailView.js.map +1 -1
  70. package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
  71. package/workflow/instance/WorkflowInstanceDetailPanel.js +2 -6
  72. package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
  73. package/workflow/instance/WorkflowInstanceList.d.ts.map +1 -1
  74. package/workflow/instance/WorkflowInstanceList.js +2 -6
  75. package/workflow/instance/WorkflowInstanceList.js.map +1 -1
  76. package/library/VisibilityToggle.d.ts +0 -53
  77. package/library/VisibilityToggle.d.ts.map +0 -1
  78. package/library/VisibilityToggle.js +0 -100
  79. package/library/VisibilityToggle.js.map +0 -1
  80. package/src/library/VisibilityToggle.tsx +0 -280
@@ -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
+ }
@@ -68,8 +68,19 @@ export type {
68
68
  export { ImportResourceDialog } from "./ImportResourceDialog";
69
69
  export type { ImportResourceDialogProps } from "./ImportResourceDialog";
70
70
 
71
- export { VisibilityToggle, VisibilityBadge } from "./VisibilityToggle";
72
- export type { VisibilityToggleProps } from "./VisibilityToggle";
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";
73
84
 
74
85
  export { ResourceVisibilityControl } from "./ResourceVisibilityControl";
75
86
  export type { ResourceVisibilityControlProps } from "./ResourceVisibilityControl";
@@ -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 visibility, and instances (AgentInstance,
39
- * WorkflowInstance) with the full private/org/public spectrum.
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
- * <VisibilityToggle
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 { VisibilityToggle } from "../library/VisibilityToggle";
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 visibilityBadge =
397
- meta?.visibility === ApiResourceVisibility.visibility_public ? (
398
- <span className="shrink-0 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium text-muted-foreground">
399
- Public
400
- </span>
401
- ) : undefined;
402
-
403
- const visibilityControl =
404
- onVisibilityChange && meta ? (
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 `<VisibilityToggle />` from the library module.
154
+ * Typically a `<ResourceVisibilityControl />` from the library module.
155
155
  * Rendered inline after the resource name.
156
156
  */
157
157
  readonly visibilityControl?: ReactNode;
@@ -235,6 +235,7 @@ export function SkillDetailView({
235
235
  kind="skill"
236
236
  resourceId={meta.id}
237
237
  visibility={meta.visibility}
238
+ org={meta.org || org}
238
239
  onChanged={refetch}
239
240
  />
240
241
  ) : undefined;
@@ -262,6 +262,7 @@ export function WorkflowDetailView({
262
262
  kind="workflow"
263
263
  resourceId={meta.id}
264
264
  visibility={meta.visibility}
265
+ org={meta.org || org}
265
266
  onChanged={refetch}
266
267
  />
267
268
  ) : undefined;