@stigmer/react 3.0.7 → 3.0.8-dev.20260612073024
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/access/ManageAccessButton.d.ts +47 -0
- package/access/ManageAccessButton.d.ts.map +1 -0
- package/access/ManageAccessButton.js +45 -0
- package/access/ManageAccessButton.js.map +1 -0
- package/access/ManageAccessDialog.d.ts +60 -0
- package/access/ManageAccessDialog.d.ts.map +1 -0
- package/access/ManageAccessDialog.js +83 -0
- package/access/ManageAccessDialog.js.map +1 -0
- package/access/index.d.ts +5 -0
- package/access/index.d.ts.map +1 -0
- package/access/index.js +7 -0
- package/access/index.js.map +1 -0
- package/access/types.d.ts +56 -0
- package/access/types.d.ts.map +1 -0
- package/access/types.js +2 -0
- package/access/types.js.map +1 -0
- package/access/useManageAccess.d.ts +60 -0
- package/access/useManageAccess.d.ts.map +1 -0
- package/access/useManageAccess.js +41 -0
- package/access/useManageAccess.js.map +1 -0
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +34 -4
- package/agent/AgentDetailView.js.map +1 -1
- package/agent-instance/AgentInstanceDetailPanel.d.ts.map +1 -1
- package/agent-instance/AgentInstanceDetailPanel.js +14 -4
- package/agent-instance/AgentInstanceDetailPanel.js.map +1 -1
- package/iam-policy/GrantAccessForm.d.ts +15 -5
- package/iam-policy/GrantAccessForm.d.ts.map +1 -1
- package/iam-policy/GrantAccessForm.js +15 -12
- package/iam-policy/GrantAccessForm.js.map +1 -1
- package/iam-policy/OrgMembersPanel.d.ts.map +1 -1
- package/iam-policy/OrgMembersPanel.js +2 -1
- package/iam-policy/OrgMembersPanel.js.map +1 -1
- package/iam-policy/PeopleWithAccess.d.ts +34 -0
- package/iam-policy/PeopleWithAccess.d.ts.map +1 -0
- package/iam-policy/PeopleWithAccess.js +55 -0
- package/iam-policy/PeopleWithAccess.js.map +1 -0
- package/iam-policy/PrincipalPicker.d.ts +48 -0
- package/iam-policy/PrincipalPicker.d.ts.map +1 -0
- package/iam-policy/PrincipalPicker.js +139 -0
- package/iam-policy/PrincipalPicker.js.map +1 -0
- package/iam-policy/ProviderBadge.d.ts +28 -0
- package/iam-policy/ProviderBadge.d.ts.map +1 -0
- package/iam-policy/ProviderBadge.js +31 -0
- package/iam-policy/ProviderBadge.js.map +1 -0
- package/iam-policy/SharePanel.d.ts +13 -5
- package/iam-policy/SharePanel.d.ts.map +1 -1
- package/iam-policy/SharePanel.js +9 -34
- package/iam-policy/SharePanel.js.map +1 -1
- package/iam-policy/index.d.ts +3 -0
- package/iam-policy/index.d.ts.map +1 -1
- package/iam-policy/index.js +3 -0
- package/iam-policy/index.js.map +1 -1
- package/index.d.ts +8 -6
- package/index.d.ts.map +1 -1
- package/index.js +6 -3
- package/index.js.map +1 -1
- package/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +41 -12
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/organization/OrgProvider.d.ts +9 -0
- package/organization/OrgProvider.d.ts.map +1 -1
- package/organization/OrgProvider.js +12 -0
- package/organization/OrgProvider.js.map +1 -1
- package/organization/index.d.ts +1 -1
- package/organization/index.d.ts.map +1 -1
- package/organization/index.js +1 -1
- package/organization/index.js.map +1 -1
- package/package.json +4 -4
- package/skill/SkillDetailView.d.ts.map +1 -1
- package/skill/SkillDetailView.js +31 -3
- package/skill/SkillDetailView.js.map +1 -1
- package/src/access/ManageAccessButton.tsx +115 -0
- package/src/access/ManageAccessDialog.tsx +239 -0
- package/src/access/__tests__/ManageAccessButton.test.tsx +62 -0
- package/src/access/__tests__/ManageAccessDialog.test.tsx +146 -0
- package/src/access/index.ts +21 -0
- package/src/access/types.ts +58 -0
- package/src/access/useManageAccess.tsx +101 -0
- package/src/agent/AgentDetailView.tsx +50 -21
- package/src/agent-instance/AgentInstanceDetailPanel.tsx +24 -42
- package/src/iam-policy/GrantAccessForm.tsx +30 -35
- package/src/iam-policy/OrgMembersPanel.tsx +2 -0
- package/src/iam-policy/PeopleWithAccess.tsx +220 -0
- package/src/iam-policy/PrincipalPicker.tsx +347 -0
- package/src/iam-policy/ProviderBadge.tsx +53 -0
- package/src/iam-policy/SharePanel.tsx +20 -165
- package/src/iam-policy/index.ts +17 -0
- package/src/index.ts +31 -0
- package/src/mcp-server/McpServerDetailView.tsx +37 -9
- package/src/organization/OrgProvider.tsx +13 -0
- package/src/organization/index.ts +1 -1
- package/src/session/__tests__/execution-target.test.ts +18 -0
- package/src/skill/SkillDetailView.tsx +34 -9
- package/src/workflow/WorkflowDetailView.tsx +49 -22
- package/src/workflow/WorkflowExecutionHeader.tsx +12 -1
- package/src/workflow/WorkflowExecutionViewer.tsx +8 -1
- package/src/workflow/index.ts +4 -0
- package/src/workflow/instance/RunVisibilityControl.tsx +116 -0
- package/src/workflow/instance/WorkflowInstanceDetailPanel.tsx +55 -42
- package/src/workflow/instance/index.ts +5 -0
- package/src/workflow/instance/useUpdateWorkflowInstanceExecutionVisibility.ts +74 -0
- package/styles.css +1 -1
- package/workflow/WorkflowDetailView.d.ts.map +1 -1
- package/workflow/WorkflowDetailView.js +31 -3
- package/workflow/WorkflowDetailView.js.map +1 -1
- package/workflow/WorkflowExecutionHeader.d.ts +7 -0
- package/workflow/WorkflowExecutionHeader.d.ts.map +1 -1
- package/workflow/WorkflowExecutionHeader.js +2 -2
- package/workflow/WorkflowExecutionHeader.js.map +1 -1
- package/workflow/WorkflowExecutionViewer.d.ts +6 -1
- package/workflow/WorkflowExecutionViewer.d.ts.map +1 -1
- package/workflow/WorkflowExecutionViewer.js +2 -2
- package/workflow/WorkflowExecutionViewer.js.map +1 -1
- package/workflow/index.d.ts +1 -1
- package/workflow/index.d.ts.map +1 -1
- package/workflow/index.js +1 -1
- package/workflow/index.js.map +1 -1
- package/workflow/instance/RunVisibilityControl.d.ts +25 -0
- package/workflow/instance/RunVisibilityControl.d.ts.map +1 -0
- package/workflow/instance/RunVisibilityControl.js +56 -0
- package/workflow/instance/RunVisibilityControl.js.map +1 -0
- package/workflow/instance/WorkflowInstanceDetailPanel.d.ts.map +1 -1
- package/workflow/instance/WorkflowInstanceDetailPanel.js +30 -4
- package/workflow/instance/WorkflowInstanceDetailPanel.js.map +1 -1
- package/workflow/instance/index.d.ts +2 -0
- package/workflow/instance/index.d.ts.map +1 -1
- package/workflow/instance/index.js +2 -0
- package/workflow/instance/index.js.map +1 -1
- package/workflow/instance/useUpdateWorkflowInstanceExecutionVisibility.d.ts +30 -0
- package/workflow/instance/useUpdateWorkflowInstanceExecutionVisibility.d.ts.map +1 -0
- package/workflow/instance/useUpdateWorkflowInstanceExecutionVisibility.js +39 -0
- package/workflow/instance/useUpdateWorkflowInstanceExecutionVisibility.js.map +1 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, cleanup } from "@testing-library/react";
|
|
3
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
4
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
5
|
+
import { ManageAccessDialog } from "../ManageAccessDialog";
|
|
6
|
+
|
|
7
|
+
// The dialog's job is composition + conditional section rendering — not the
|
|
8
|
+
// behavior of the lower-level controls, which own their own tests. Stub the
|
|
9
|
+
// two heavy children to assert *which* sections mount, and pin the
|
|
10
|
+
// proto-generated capability so the People gate is deterministic.
|
|
11
|
+
const hasGrantableRolesMock = vi.fn<(kind: ApiResourceKind) => boolean>(() => true);
|
|
12
|
+
vi.mock("@stigmer/sdk", async (importActual) => {
|
|
13
|
+
const actual = await importActual<typeof import("@stigmer/sdk")>();
|
|
14
|
+
return { ...actual, hasGrantableRoles: (kind: ApiResourceKind) => hasGrantableRolesMock(kind) };
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
vi.mock("../../iam-policy/PeopleWithAccess", () => ({
|
|
18
|
+
PeopleWithAccess: () => <div data-testid="people-with-access" />,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("../../library/ResourceVisibilityControl", () => ({
|
|
22
|
+
ResourceVisibilityControl: () => <div data-testid="visibility-control" />,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
// happy-dom does not implement the native dialog show/close methods.
|
|
26
|
+
beforeAll(() => {
|
|
27
|
+
HTMLDialogElement.prototype.showModal = function showModal() {
|
|
28
|
+
this.open = true;
|
|
29
|
+
};
|
|
30
|
+
HTMLDialogElement.prototype.close = function close() {
|
|
31
|
+
this.open = false;
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
cleanup();
|
|
37
|
+
hasGrantableRolesMock.mockReset();
|
|
38
|
+
hasGrantableRolesMock.mockReturnValue(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const RESOURCE = {
|
|
42
|
+
kind: ApiResourceKind.agent,
|
|
43
|
+
kindString: "agent",
|
|
44
|
+
id: "agt_123",
|
|
45
|
+
org: "acme",
|
|
46
|
+
name: "Release Bot",
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
describe("ManageAccessDialog", () => {
|
|
50
|
+
it("mounts no body while closed (the access-list fetch stays lazy)", () => {
|
|
51
|
+
render(
|
|
52
|
+
<ManageAccessDialog
|
|
53
|
+
open={false}
|
|
54
|
+
onOpenChange={() => {}}
|
|
55
|
+
resource={RESOURCE}
|
|
56
|
+
/>,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(screen.queryByText("Manage access")).toBeNull();
|
|
60
|
+
expect(screen.queryByTestId("people-with-access")).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("renders the header with the resource name when open", () => {
|
|
64
|
+
render(
|
|
65
|
+
<ManageAccessDialog open onOpenChange={() => {}} resource={RESOURCE} />,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(screen.getByText("Manage access")).toBeTruthy();
|
|
69
|
+
expect(screen.getByText("Release Bot")).toBeTruthy();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("renders General access only when a visibility descriptor is provided", () => {
|
|
73
|
+
const { rerender } = render(
|
|
74
|
+
<ManageAccessDialog open onOpenChange={() => {}} resource={RESOURCE} />,
|
|
75
|
+
);
|
|
76
|
+
expect(screen.queryByTestId("visibility-control")).toBeNull();
|
|
77
|
+
|
|
78
|
+
rerender(
|
|
79
|
+
<ManageAccessDialog
|
|
80
|
+
open
|
|
81
|
+
onOpenChange={() => {}}
|
|
82
|
+
resource={RESOURCE}
|
|
83
|
+
visibility={{
|
|
84
|
+
kind: "agent",
|
|
85
|
+
current: ApiResourceVisibility.visibility_private,
|
|
86
|
+
org: "acme",
|
|
87
|
+
}}
|
|
88
|
+
/>,
|
|
89
|
+
);
|
|
90
|
+
expect(screen.getByTestId("visibility-control")).toBeTruthy();
|
|
91
|
+
expect(screen.getByText("General access")).toBeTruthy();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("renders People only when the kind has grantable roles", () => {
|
|
95
|
+
hasGrantableRolesMock.mockReturnValue(false);
|
|
96
|
+
const { rerender } = render(
|
|
97
|
+
<ManageAccessDialog open onOpenChange={() => {}} resource={RESOURCE} />,
|
|
98
|
+
);
|
|
99
|
+
expect(screen.queryByTestId("people-with-access")).toBeNull();
|
|
100
|
+
|
|
101
|
+
hasGrantableRolesMock.mockReturnValue(true);
|
|
102
|
+
rerender(
|
|
103
|
+
<ManageAccessDialog
|
|
104
|
+
open
|
|
105
|
+
onOpenChange={() => {}}
|
|
106
|
+
resource={{ ...RESOURCE, id: "agt_456" }}
|
|
107
|
+
/>,
|
|
108
|
+
);
|
|
109
|
+
expect(screen.getByTestId("people-with-access")).toBeTruthy();
|
|
110
|
+
expect(screen.getByText("People with access")).toBeTruthy();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("renders the extra section only when provided", () => {
|
|
114
|
+
render(
|
|
115
|
+
<ManageAccessDialog
|
|
116
|
+
open
|
|
117
|
+
onOpenChange={() => {}}
|
|
118
|
+
resource={RESOURCE}
|
|
119
|
+
extraSection={{
|
|
120
|
+
title: "Run visibility",
|
|
121
|
+
description: "Who can observe runs.",
|
|
122
|
+
content: <div data-testid="run-visibility" />,
|
|
123
|
+
}}
|
|
124
|
+
/>,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
expect(screen.getByText("Run visibility")).toBeTruthy();
|
|
128
|
+
expect(screen.getByTestId("run-visibility")).toBeTruthy();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("requests close via Done and the close affordance", () => {
|
|
132
|
+
const onOpenChange = vi.fn();
|
|
133
|
+
render(
|
|
134
|
+
<ManageAccessDialog open onOpenChange={onOpenChange} resource={RESOURCE} />,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Query by text/label rather than role: a native <dialog> that has not yet
|
|
138
|
+
// run showModal() hides its descendants from the accessibility tree.
|
|
139
|
+
screen.getByText("Done").click();
|
|
140
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
141
|
+
|
|
142
|
+
onOpenChange.mockClear();
|
|
143
|
+
screen.getByLabelText("Close").click();
|
|
144
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Access — the unified "Manage access" experience composing visibility
|
|
2
|
+
// (library) and explicit grants (iam-policy) into one dialog, with a kebab
|
|
3
|
+
// hook and a visible-button trigger for the surfaces that mount it.
|
|
4
|
+
export {
|
|
5
|
+
ManageAccessDialog,
|
|
6
|
+
type ManageAccessDialogProps,
|
|
7
|
+
} from "./ManageAccessDialog";
|
|
8
|
+
export {
|
|
9
|
+
ManageAccessButton,
|
|
10
|
+
type ManageAccessButtonProps,
|
|
11
|
+
} from "./ManageAccessButton";
|
|
12
|
+
export {
|
|
13
|
+
useManageAccess,
|
|
14
|
+
type UseManageAccessArgs,
|
|
15
|
+
type UseManageAccessReturn,
|
|
16
|
+
} from "./useManageAccess";
|
|
17
|
+
export type {
|
|
18
|
+
AccessResource,
|
|
19
|
+
AccessVisibility,
|
|
20
|
+
AccessExtraSection,
|
|
21
|
+
} from "./types";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
3
|
+
import type { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
4
|
+
import type { VisibilityResourceKind } from "../library/useUpdateVisibility";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The resource whose access is being managed. Carries everything the People
|
|
8
|
+
* section needs: the enum {@link ApiResourceKind} (for grantable-role lookup),
|
|
9
|
+
* the FGA/API kind string (for IAM refs and permission checks), the id, and
|
|
10
|
+
* the owning org (drives the org-member typeahead).
|
|
11
|
+
*/
|
|
12
|
+
export interface AccessResource {
|
|
13
|
+
/** ApiResourceKind enum — drives grantable-role lookup and capability. */
|
|
14
|
+
readonly kind: ApiResourceKind;
|
|
15
|
+
/** FGA/API kind string (e.g. "mcp_server", "session", "workflow_execution"). */
|
|
16
|
+
readonly kindString: string;
|
|
17
|
+
/** Resource id. */
|
|
18
|
+
readonly id: string;
|
|
19
|
+
/** Slug of the owning organization (`metadata.org`). */
|
|
20
|
+
readonly org: string;
|
|
21
|
+
/** Optional display name, shown as the dialog subtitle for context. */
|
|
22
|
+
readonly name?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Describes the "General access" (visibility) axis for the Manage access
|
|
27
|
+
* dialog. Optional because not every resource has visibility (e.g. sessions
|
|
28
|
+
* and workflow executions do not). When present, the dialog renders the
|
|
29
|
+
* shared `ResourceVisibilityControl`, which owns level selection and
|
|
30
|
+
* the `can_edit` gate.
|
|
31
|
+
*/
|
|
32
|
+
export interface AccessVisibility {
|
|
33
|
+
/** Resource kind, selecting both the updateVisibility RPC and FGA type. */
|
|
34
|
+
readonly kind: VisibilityResourceKind;
|
|
35
|
+
/** Current visibility of the resource. */
|
|
36
|
+
readonly current: ApiResourceVisibility;
|
|
37
|
+
/**
|
|
38
|
+
* Slug of the owning org (`metadata.org`); gates the Platform option for
|
|
39
|
+
* blueprints. Omit for instances and where Platform should not be offered.
|
|
40
|
+
*/
|
|
41
|
+
readonly org?: string;
|
|
42
|
+
/** Called after a successful visibility change so the host can refetch. */
|
|
43
|
+
readonly onChanged?: () => void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A generic, resource-specific access section appended below People — the
|
|
48
|
+
* escape hatch for the rare per-kind axis (today: workflow-instance run
|
|
49
|
+
* observability) without baking that knowledge into a generic dialog.
|
|
50
|
+
*/
|
|
51
|
+
export interface AccessExtraSection {
|
|
52
|
+
/** Section heading. */
|
|
53
|
+
readonly title: string;
|
|
54
|
+
/** Optional one-line explanation under the heading. */
|
|
55
|
+
readonly description?: string;
|
|
56
|
+
/** The section body (e.g. a `<RunVisibilityControl />`). */
|
|
57
|
+
readonly content: ReactNode;
|
|
58
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState, type ReactNode } from "react";
|
|
4
|
+
import { useCheckPermission } from "../iam-policy/useCheckPermission";
|
|
5
|
+
import type { DetailAction } from "../resource-detail/types";
|
|
6
|
+
import { ManageAccessDialog } from "./ManageAccessDialog";
|
|
7
|
+
import type {
|
|
8
|
+
AccessResource,
|
|
9
|
+
AccessVisibility,
|
|
10
|
+
AccessExtraSection,
|
|
11
|
+
} from "./types";
|
|
12
|
+
|
|
13
|
+
/** Arguments for {@link useManageAccess}. */
|
|
14
|
+
export interface UseManageAccessArgs {
|
|
15
|
+
/**
|
|
16
|
+
* The resource whose access is managed, or `null` while it is still
|
|
17
|
+
* loading. When `null`, {@link UseManageAccessReturn.action} is `null` and
|
|
18
|
+
* the dialog renders nothing — safe to call before the resource is ready.
|
|
19
|
+
*/
|
|
20
|
+
readonly resource: AccessResource | null;
|
|
21
|
+
/** General access (visibility) axis; omit for resources without visibility. */
|
|
22
|
+
readonly visibility?: AccessVisibility;
|
|
23
|
+
/** Optional resource-specific section (e.g. run observability). */
|
|
24
|
+
readonly extraSection?: AccessExtraSection;
|
|
25
|
+
/** Menu-item label. @default "Manage access" */
|
|
26
|
+
readonly label?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Return value of {@link useManageAccess}. */
|
|
30
|
+
export interface UseManageAccessReturn {
|
|
31
|
+
/**
|
|
32
|
+
* A ready-to-spread {@link DetailAction} for a kebab/overflow menu, or
|
|
33
|
+
* `null` when the resource is unavailable or the user lacks
|
|
34
|
+
* `can_view_access`. Lives in the `"sharing"` group.
|
|
35
|
+
*/
|
|
36
|
+
readonly action: DetailAction | null;
|
|
37
|
+
/** The {@link ManageAccessDialog} node — render it once in the host tree. */
|
|
38
|
+
readonly dialog: ReactNode;
|
|
39
|
+
/** Imperatively open the dialog. */
|
|
40
|
+
readonly open: () => void;
|
|
41
|
+
/** Whether the dialog is currently open. */
|
|
42
|
+
readonly isOpen: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Wires the unified Manage access dialog to a kebab/overflow menu — the
|
|
47
|
+
* trigger shape used by static resource detail views (agent, skill,
|
|
48
|
+
* mcp_server, workflow), whose actions live in {@link ResourceActionBar}'s
|
|
49
|
+
* menu rather than as a standalone button.
|
|
50
|
+
*
|
|
51
|
+
* Owns the open-state and the `can_view_access` gate (a viewer may open the
|
|
52
|
+
* dialog to read general access and the people list; the dialog's own sections
|
|
53
|
+
* gate editing). Returns a `null` action when the resource is still loading or
|
|
54
|
+
* the user cannot view access — so the host can unconditionally fold
|
|
55
|
+
* `action` into its actions array.
|
|
56
|
+
*
|
|
57
|
+
* Surfaces that want a *visible* trigger instead use {@link ManageAccessButton}.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```tsx
|
|
61
|
+
* const access = useManageAccess({
|
|
62
|
+
* resource: meta ? { kind: ApiResourceKind.agent, kindString: "agent", id: meta.id, org: meta.org } : null,
|
|
63
|
+
* visibility: meta ? { kind: "agent", current: meta.visibility, org: meta.org, onChanged: refetch } : undefined,
|
|
64
|
+
* });
|
|
65
|
+
* // ...
|
|
66
|
+
* <ResourceDetailShell actions={access.action ? [...actions, access.action] : actions} ... />
|
|
67
|
+
* {access.dialog}
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function useManageAccess({
|
|
71
|
+
resource,
|
|
72
|
+
visibility,
|
|
73
|
+
extraSection,
|
|
74
|
+
label = "Manage access",
|
|
75
|
+
}: UseManageAccessArgs): UseManageAccessReturn {
|
|
76
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
77
|
+
|
|
78
|
+
const { allowed: canView } = useCheckPermission(
|
|
79
|
+
resource ? { kind: resource.kindString, id: resource.id } : null,
|
|
80
|
+
"can_view_access",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const open = useCallback(() => setIsOpen(true), []);
|
|
84
|
+
|
|
85
|
+
const action: DetailAction | null =
|
|
86
|
+
resource && canView
|
|
87
|
+
? { id: "manage-access", label, group: "sharing", onAction: open }
|
|
88
|
+
: null;
|
|
89
|
+
|
|
90
|
+
const dialog = resource ? (
|
|
91
|
+
<ManageAccessDialog
|
|
92
|
+
open={isOpen}
|
|
93
|
+
onOpenChange={setIsOpen}
|
|
94
|
+
resource={resource}
|
|
95
|
+
visibility={visibility}
|
|
96
|
+
extraSection={extraSection}
|
|
97
|
+
/>
|
|
98
|
+
) : null;
|
|
99
|
+
|
|
100
|
+
return { action, dialog, open, isOpen };
|
|
101
|
+
}
|
|
@@ -10,11 +10,13 @@ import type {
|
|
|
10
10
|
import type { ApiResourceReference } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
11
11
|
import type { EnvVarDeclaration } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
|
|
12
12
|
import type { AgentInstance } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
|
|
13
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
13
14
|
import { useAgent } from "./useAgent";
|
|
14
15
|
import { useUpdateAgent } from "./useUpdateAgent";
|
|
15
16
|
import { agentToInput } from "./internal/agentToInput";
|
|
16
17
|
import { ErrorMessage } from "../error/ErrorMessage";
|
|
17
|
-
import {
|
|
18
|
+
import { VisibilityBadge } from "../library/VisibilitySelector";
|
|
19
|
+
import { useManageAccess } from "../access/useManageAccess";
|
|
18
20
|
import { ResourceDetailShell } from "../resource-detail/ResourceDetailShell";
|
|
19
21
|
import { Section } from "../resource-detail/Section";
|
|
20
22
|
import { useDetailTabs } from "../resource-detail/useDetailTabs";
|
|
@@ -280,6 +282,30 @@ export function AgentDetailView({
|
|
|
280
282
|
}
|
|
281
283
|
}, [agent]);
|
|
282
284
|
|
|
285
|
+
// Unified Manage access — visibility (General access) over explicit grants
|
|
286
|
+
// (People), opened from the kebab. Derived from the loaded metadata, so the
|
|
287
|
+
// action stays null until the resource is ready (and folds harmlessly into
|
|
288
|
+
// the actions array meanwhile).
|
|
289
|
+
const access = useManageAccess({
|
|
290
|
+
resource: agent?.metadata
|
|
291
|
+
? {
|
|
292
|
+
kind: ApiResourceKind.agent,
|
|
293
|
+
kindString: "agent",
|
|
294
|
+
id: agent.metadata.id,
|
|
295
|
+
org: agent.metadata.org,
|
|
296
|
+
name: agent.metadata.name,
|
|
297
|
+
}
|
|
298
|
+
: null,
|
|
299
|
+
visibility: agent?.metadata
|
|
300
|
+
? {
|
|
301
|
+
kind: "agent",
|
|
302
|
+
current: agent.metadata.visibility,
|
|
303
|
+
org: agent.metadata.org,
|
|
304
|
+
onChanged: refetch,
|
|
305
|
+
}
|
|
306
|
+
: undefined,
|
|
307
|
+
});
|
|
308
|
+
|
|
283
309
|
if (isLoading) return <LoadingSkeleton className={className} />;
|
|
284
310
|
if (error)
|
|
285
311
|
return <ErrorMessage error={error} retry={refetch} className={className} />;
|
|
@@ -312,16 +338,16 @@ export function AgentDetailView({
|
|
|
312
338
|
updatedAt: specAudit?.updatedAt ? timestampDate(specAudit.updatedAt) : null,
|
|
313
339
|
};
|
|
314
340
|
|
|
341
|
+
// Inline visibility is read-only (at-a-glance); editing lives in the
|
|
342
|
+
// Manage access dialog, the single writer for both access axes.
|
|
315
343
|
const visibilityControl = meta ? (
|
|
316
|
-
<
|
|
317
|
-
kind="agent"
|
|
318
|
-
resourceId={meta.id}
|
|
319
|
-
visibility={meta.visibility}
|
|
320
|
-
org={meta.org || org}
|
|
321
|
-
onChanged={refetch}
|
|
322
|
-
/>
|
|
344
|
+
<VisibilityBadge visibility={meta.visibility} />
|
|
323
345
|
) : undefined;
|
|
324
346
|
|
|
347
|
+
const mergedActions = access.action
|
|
348
|
+
? [...(actions ?? []), access.action]
|
|
349
|
+
: actions;
|
|
350
|
+
|
|
325
351
|
let tabContent: React.ReactNode;
|
|
326
352
|
if (activeAdditionalTab) {
|
|
327
353
|
tabContent = activeAdditionalTab.content;
|
|
@@ -361,19 +387,22 @@ export function AgentDetailView({
|
|
|
361
387
|
}
|
|
362
388
|
|
|
363
389
|
return (
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
390
|
+
<>
|
|
391
|
+
<ResourceDetailShell
|
|
392
|
+
header={headerMeta}
|
|
393
|
+
visibilityControl={visibilityControl}
|
|
394
|
+
primaryAction={primaryAction}
|
|
395
|
+
actions={mergedActions}
|
|
396
|
+
tabs={effectiveTabs}
|
|
397
|
+
activeTab={effectiveTabs ? effectiveActiveTab : undefined}
|
|
398
|
+
onTabChange={effectiveTabs ? effectiveOnTabChange : undefined}
|
|
399
|
+
tabsAriaLabel="Agent detail sections"
|
|
400
|
+
className={className}
|
|
401
|
+
>
|
|
402
|
+
{tabContent}
|
|
403
|
+
</ResourceDetailShell>
|
|
404
|
+
{access.dialog}
|
|
405
|
+
</>
|
|
377
406
|
);
|
|
378
407
|
}
|
|
379
408
|
|
|
@@ -10,9 +10,9 @@ import type { ResourceRef } from "@stigmer/sdk";
|
|
|
10
10
|
import { getUserMessage } from "@stigmer/sdk";
|
|
11
11
|
import { useUpdateAgentInstance } from "./useUpdateAgentInstance";
|
|
12
12
|
import { useDeleteAgentInstance } from "./useDeleteAgentInstance";
|
|
13
|
-
import {
|
|
13
|
+
import { VisibilityBadge } from "../library/VisibilitySelector";
|
|
14
14
|
import { PermissionGate } from "../iam-policy/PermissionGate";
|
|
15
|
-
import {
|
|
15
|
+
import { ManageAccessButton } from "../access/ManageAccessButton";
|
|
16
16
|
import { EnvironmentPicker } from "../environment/EnvironmentPicker";
|
|
17
17
|
import { useEnvironmentList } from "../environment/useEnvironmentList";
|
|
18
18
|
|
|
@@ -58,10 +58,11 @@ export function AgentInstanceDetailPanel({
|
|
|
58
58
|
|
|
59
59
|
const [isEditingEnvs, setIsEditingEnvs] = useState(false);
|
|
60
60
|
const [editEnvRefs, setEditEnvRefs] = useState<ResourceRef[]>([]);
|
|
61
|
-
const [showSharePanel, setShowSharePanel] = useState(false);
|
|
62
61
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
63
62
|
const [deleteError, setDeleteError] = useState<Error | null>(null);
|
|
64
63
|
|
|
64
|
+
const visibility = meta?.visibility ?? ApiResourceVisibility.visibility_private;
|
|
65
|
+
|
|
65
66
|
const handleStartEditEnvs = useCallback(() => {
|
|
66
67
|
const currentRefs: ResourceRef[] = (spec?.environmentRefs ?? []).map((ref) => ({
|
|
67
68
|
org: ref.org || org,
|
|
@@ -115,9 +116,12 @@ export function AgentInstanceDetailPanel({
|
|
|
115
116
|
{/* Header */}
|
|
116
117
|
<div className="flex items-center justify-between border-b border-border px-4 py-3">
|
|
117
118
|
<div>
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
<div className="flex items-center gap-2">
|
|
120
|
+
<h3 className="text-sm font-semibold text-foreground">
|
|
121
|
+
{meta?.name || meta?.slug || "Instance"}
|
|
122
|
+
</h3>
|
|
123
|
+
<VisibilityBadge visibility={visibility} />
|
|
124
|
+
</div>
|
|
121
125
|
<p className="text-[0.65rem] text-muted-foreground">
|
|
122
126
|
{createdAt && `Created ${createdAt.toLocaleDateString()}`}
|
|
123
127
|
{updatedAt && ` \u00B7 Updated ${updatedAt.toLocaleDateString()}`}
|
|
@@ -137,19 +141,20 @@ export function AgentInstanceDetailPanel({
|
|
|
137
141
|
Start session
|
|
138
142
|
</button>
|
|
139
143
|
)}
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
144
|
+
<ManageAccessButton
|
|
145
|
+
resource={{
|
|
146
|
+
kind: ApiResourceKind.agent_instance,
|
|
147
|
+
kindString: "agent_instance",
|
|
148
|
+
id,
|
|
149
|
+
org: meta?.org ?? "",
|
|
150
|
+
name: meta?.name,
|
|
151
|
+
}}
|
|
152
|
+
visibility={{
|
|
153
|
+
kind: "agentInstance",
|
|
154
|
+
current: visibility,
|
|
155
|
+
onChanged: onUpdated,
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
153
158
|
<button
|
|
154
159
|
type="button"
|
|
155
160
|
onClick={onClose}
|
|
@@ -241,29 +246,6 @@ export function AgentInstanceDetailPanel({
|
|
|
241
246
|
)}
|
|
242
247
|
</div>
|
|
243
248
|
|
|
244
|
-
{/* Visibility */}
|
|
245
|
-
<div className="px-4 py-3">
|
|
246
|
-
<h4 className="text-xs font-medium text-muted-foreground mb-2">Visibility</h4>
|
|
247
|
-
<ResourceVisibilityControl
|
|
248
|
-
kind="agentInstance"
|
|
249
|
-
resourceId={id}
|
|
250
|
-
visibility={meta?.visibility ?? ApiResourceVisibility.visibility_private}
|
|
251
|
-
onChanged={onUpdated}
|
|
252
|
-
/>
|
|
253
|
-
</div>
|
|
254
|
-
|
|
255
|
-
{/* Share Panel */}
|
|
256
|
-
{showSharePanel && (
|
|
257
|
-
<div className="px-4 py-3">
|
|
258
|
-
<SharePanel
|
|
259
|
-
resource={{ kind: "agent_instance", id, resourceKind: ApiResourceKind.agent_instance }}
|
|
260
|
-
resourceKindString="agent_instance"
|
|
261
|
-
resourceKind={ApiResourceKind.agent_instance}
|
|
262
|
-
onClose={() => setShowSharePanel(false)}
|
|
263
|
-
/>
|
|
264
|
-
</div>
|
|
265
|
-
)}
|
|
266
|
-
|
|
267
249
|
{/* Delete */}
|
|
268
250
|
<PermissionGate resource={{ kind: "agent_instance", id }} relation="can_delete">
|
|
269
251
|
<div className="px-4 py-3">
|
|
@@ -11,6 +11,7 @@ import { cn } from "@stigmer/theme";
|
|
|
11
11
|
import { getUserMessage, iamRoleToString } from "@stigmer/sdk";
|
|
12
12
|
import { useCreateIamPolicy } from "./useCreateIamPolicy";
|
|
13
13
|
import { RoleSelector } from "./RoleSelector";
|
|
14
|
+
import { PrincipalPicker, type SelectedPrincipal } from "./PrincipalPicker";
|
|
14
15
|
|
|
15
16
|
/** Props for {@link GrantAccessForm}. */
|
|
16
17
|
export interface GrantAccessFormProps {
|
|
@@ -20,6 +21,13 @@ export interface GrantAccessFormProps {
|
|
|
20
21
|
readonly resourceKindString: string;
|
|
21
22
|
/** ID of the resource being granted access to. */
|
|
22
23
|
readonly resourceId: string;
|
|
24
|
+
/**
|
|
25
|
+
* Organization whose members can be granted access. Drives the
|
|
26
|
+
* {@link PrincipalPicker} typeahead.
|
|
27
|
+
*/
|
|
28
|
+
readonly orgId: string;
|
|
29
|
+
/** Principal IDs that already have access (shown disabled in the picker). */
|
|
30
|
+
readonly excludePrincipalIds?: readonly string[];
|
|
23
31
|
/** Fired after a policy is successfully created. */
|
|
24
32
|
readonly onGranted?: (policy: IamPolicy) => void;
|
|
25
33
|
/** Fired when the user cancels. */
|
|
@@ -29,11 +37,13 @@ export interface GrantAccessFormProps {
|
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
/**
|
|
32
|
-
* Form for granting
|
|
40
|
+
* Form for granting an organization member access to a resource.
|
|
33
41
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* policy
|
|
42
|
+
* Lets the user pick a **person** from the org's member list (by name or
|
|
43
|
+
* email, disambiguating identity sources) via {@link PrincipalPicker}, choose
|
|
44
|
+
* a **role** from the resource's grantable roles, and creates the IAM policy
|
|
45
|
+
* binding. The resolved `identity_account` ID is carried internally — the user
|
|
46
|
+
* never types or sees raw account IDs.
|
|
37
47
|
*
|
|
38
48
|
* All visual properties flow through `--stgm-*` design tokens.
|
|
39
49
|
*
|
|
@@ -43,6 +53,7 @@ export interface GrantAccessFormProps {
|
|
|
43
53
|
* resourceKind={ApiResourceKind.organization}
|
|
44
54
|
* resourceKindString="organization"
|
|
45
55
|
* resourceId="org-abc123"
|
|
56
|
+
* orgId="org-abc123"
|
|
46
57
|
* onGranted={(policy) => refetchAccessList()}
|
|
47
58
|
* onCancel={() => setShowForm(false)}
|
|
48
59
|
* />
|
|
@@ -52,6 +63,8 @@ export function GrantAccessForm({
|
|
|
52
63
|
resourceKind,
|
|
53
64
|
resourceKindString,
|
|
54
65
|
resourceId,
|
|
66
|
+
orgId,
|
|
67
|
+
excludePrincipalIds,
|
|
55
68
|
onGranted,
|
|
56
69
|
onCancel,
|
|
57
70
|
className,
|
|
@@ -59,24 +72,23 @@ export function GrantAccessForm({
|
|
|
59
72
|
const { create: createPolicy, isCreating, error, clearError } =
|
|
60
73
|
useCreateIamPolicy();
|
|
61
74
|
|
|
62
|
-
const [
|
|
75
|
+
const [principal, setPrincipal] = useState<SelectedPrincipal | null>(null);
|
|
63
76
|
const [selectedRole, setSelectedRole] = useState<IamRole | null>(null);
|
|
64
77
|
|
|
65
|
-
const trimmedPrincipalId = principalId.trim();
|
|
66
78
|
const canSubmit =
|
|
67
|
-
|
|
79
|
+
principal !== null && selectedRole !== null && !isCreating;
|
|
68
80
|
|
|
69
81
|
const handleSubmit = useCallback(
|
|
70
82
|
async (e: FormEvent) => {
|
|
71
83
|
e.preventDefault();
|
|
72
|
-
if (!canSubmit || selectedRole === null) return;
|
|
84
|
+
if (!canSubmit || selectedRole === null || principal === null) return;
|
|
73
85
|
|
|
74
86
|
clearError();
|
|
75
87
|
try {
|
|
76
88
|
const spec = create(IamPolicySpecSchema, {
|
|
77
89
|
principal: create(ApiResourceRefSchema, {
|
|
78
90
|
kind: "identity_account",
|
|
79
|
-
id:
|
|
91
|
+
id: principal.id,
|
|
80
92
|
}),
|
|
81
93
|
resource: create(ApiResourceRefSchema, {
|
|
82
94
|
kind: resourceKindString,
|
|
@@ -93,7 +105,7 @@ export function GrantAccessForm({
|
|
|
93
105
|
[
|
|
94
106
|
canSubmit,
|
|
95
107
|
selectedRole,
|
|
96
|
-
|
|
108
|
+
principal,
|
|
97
109
|
resourceKindString,
|
|
98
110
|
resourceId,
|
|
99
111
|
createPolicy,
|
|
@@ -105,31 +117,14 @@ export function GrantAccessForm({
|
|
|
105
117
|
return (
|
|
106
118
|
<form onSubmit={handleSubmit} className={cn("space-y-3", className)}>
|
|
107
119
|
<div className="space-y-3">
|
|
108
|
-
{/* Principal */}
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
<input
|
|
117
|
-
id="stgm-grant-principal"
|
|
118
|
-
type="text"
|
|
119
|
-
value={principalId}
|
|
120
|
-
onChange={(e) => setPrincipalId(e.target.value)}
|
|
121
|
-
placeholder="e.g. ia-01HQUSER123"
|
|
122
|
-
disabled={isCreating}
|
|
123
|
-
autoFocus
|
|
124
|
-
required
|
|
125
|
-
className={cn(
|
|
126
|
-
"w-full rounded-md border border-input bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
127
|
-
"placeholder:text-muted-foreground",
|
|
128
|
-
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
|
|
129
|
-
"disabled:pointer-events-none disabled:opacity-50",
|
|
130
|
-
)}
|
|
131
|
-
/>
|
|
132
|
-
</div>
|
|
120
|
+
{/* Principal picker (org-member typeahead) */}
|
|
121
|
+
<PrincipalPicker
|
|
122
|
+
orgId={orgId}
|
|
123
|
+
value={principal}
|
|
124
|
+
onChange={setPrincipal}
|
|
125
|
+
excludePrincipalIds={excludePrincipalIds}
|
|
126
|
+
disabled={isCreating}
|
|
127
|
+
/>
|
|
133
128
|
|
|
134
129
|
{/* Role selector */}
|
|
135
130
|
<RoleSelector
|