Skip to content

Commit ad99c5d

Browse files
authored
feat(permissions): MCP settings page (#2978)
* feat: MCP settings paget pu * chore: remove playbook and view table for MCP * chore: refactor file paths * fix(permissions): fix SubjectSelectorModal bugs and UX issues - Fix re-render loop: init selected state only on open transition via prevOpenRef, read preselectedSubjectIds through a stable ref so parent re-renders don't wipe user selections - Fix incomplete onAllow payload: add fetchPermissionSubjectsByIds and seed selectedSubjects upfront so preselected subjects on other pages are always included - Allow applying empty selection (remove last subject) by disabling Apply only when selection is unchanged from initial, not when count is zero - Debounce search input with useDebouncedValue(300ms) to avoid per-keystroke API calls - Remove unnecessary useMemo around selectedCount - Add PAGE_SIZE to query key - Replace Avatar with type-appropriate icon for team/group subjects - Replace typeLabel function with a TYPE_LABELS record map - Wrap subjects derivation in useMemo to fix exhaustive-deps lint warning * fix(permissions): improve PermissionAccessCard UX and correctness * feat(mcp): redesign overview subject access controls * fix(mcp): use playbook object selectors and dedupe overrides * fix(mcp): centralize and enforce mcp settings source * fix(mcp): resolve ambiguous view-name permission matching * chore: refactor access card -> permission matching * fix(mcp): centralize tab-body loading and avoid blank switch * fix(mcp): cap card subjects and scope queries to mcp source * feat(mcp): show effective access verdicts in subject viewer * modify subject selector access modal * remove card design * feat(permissions): compact MCP access rows and add switch sizes * feat(mcp): move subject selector to sticky side panel * fix: mcp overview page * fix: width of cards * fix: type error in test * fix: review * chore: refactor * feat: Display setup MCP token button in mcp settings * fix(permissions): address PR review issues in MCP permission UI * fix(permissions): handle missing subjects and always refetch overrides * fix: show MCP token creation error inside the modal * Add playbook permission access check modal with shared resource selector * fix(permissions): allow mouse selection in access-check modal * refactor(permissions): centralize subject avatar rendering * fix: highlight selected rows * feat: grouping playbooks by category * fix(mcp-permissions): improve subject selector switch feedback * feat(mcp): add check access tab with conditional resources * feat(permissions): support access-token subjects in selector UI * fix(mcp): reduce subject avatar size in overview list * feat: Subject -> mcp settings * parity between playbooks and users page * feat(permissions): add effective access checks across MCP tabs * fix(mcp): reuse tokens table on overview without layout gap * fix(permissions): align selector panel bulk-lock UX * fix(permissions): match effective access and mode control widths * sorting * sorting animation * change sort menu * feat(permissions): add subject sort controls with direction toggle * feat(permissions): split resource selector lists by type Extract ResourceList, ResourceRow, and EffectiveAccessBadge from ResourceSelectorPanel. Add independent bulk access switches for Playbooks and Views. * perf(permissions): localize list sorting to reduce flicker Move sort state and sorting logic into each ResourceList so sorting one list does not rerender the sibling list. Memoize ResourceList and ResourceRow and stabilize bulk handlers in the parent. * feat(permissions): add subjects tab with shared subject panel
1 parent 8f6de84 commit ad99c5d

43 files changed

Lines changed: 5701 additions & 149 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package-lock.json

Lines changed: 102 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"@radix-ui/react-select": "^2.2.6",
4141
"@radix-ui/react-separator": "^1.1.8",
4242
"@radix-ui/react-slot": "^1.2.4",
43+
"@radix-ui/react-switch": "^1.2.6",
4344
"@radix-ui/react-tooltip": "^1.2.8",
4445
"@radix-ui/react-use-controllable-state": "^1.2.2",
4546
"@storybook/client-api": "^7.6.17",

src/App.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ import { UserAccessStateContextProvider } from "./context/UserAccessContext/User
4747
import { tables } from "./context/UserAccessContext/permissions";
4848

4949
import { PermissionsPage } from "./pages/Settings/PermissionsPage";
50+
import { PermissionsSubjectsPage } from "./pages/Settings/PermissionsSubjectsPage";
51+
import McpOverviewPage from "./pages/Settings/mcp/McpOverviewPage";
52+
import McpPlaybooksPage from "./pages/Settings/mcp/McpPlaybooksPage";
53+
import McpViewsPage from "./pages/Settings/mcp/McpViewsPage";
54+
import McpSubjectAccessPage from "./pages/Settings/mcp/McpSubjectAccessPage";
55+
import McpCheckAccessPage from "./pages/Settings/mcp/McpCheckAccessPage";
5056
import ScopesPage from "./pages/Settings/ScopesPage";
5157
import { features } from "./services/permissions/features";
5258
import { getViewsForSidebar, ViewSummary } from "./api/services/views";
@@ -427,6 +433,15 @@ const settingsNav: SettingsNavigationItems = {
427433
featureName: features["settings.job_history"],
428434
resourceName: tables.database
429435
},
436+
{
437+
name: "MCP",
438+
href: "/settings/mcp",
439+
icon: ({ className }: { className: string }) => (
440+
<Icon name="mcp" className={`${className} [&_path]:!fill-white`} />
441+
),
442+
featureName: features["settings.mcp"],
443+
resourceName: tables.database
444+
},
430445
{
431446
name: "Feature Flags",
432447
href: "/settings/feature-flags",
@@ -735,6 +750,14 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) {
735750
"read"
736751
)}
737752
/>
753+
<Route
754+
path="permissions/subjects"
755+
element={withAuthorizationAccessCheck(
756+
<PermissionsSubjectsPage />,
757+
tables.permissions,
758+
"read"
759+
)}
760+
/>
738761
<Route
739762
path="scopes"
740763
element={withAuthorizationAccessCheck(
@@ -841,6 +864,55 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) {
841864
/>
842865
</Route>
843866

867+
<Route path="mcp">
868+
<Route index element={<Navigate to="/settings/mcp/overview" />} />
869+
<Route
870+
path="overview"
871+
element={withAuthorizationAccessCheck(
872+
<McpOverviewPage />,
873+
tables.database,
874+
"write",
875+
true
876+
)}
877+
/>
878+
<Route
879+
path="playbooks"
880+
element={withAuthorizationAccessCheck(
881+
<McpPlaybooksPage />,
882+
tables.database,
883+
"write",
884+
true
885+
)}
886+
/>
887+
<Route
888+
path="views"
889+
element={withAuthorizationAccessCheck(
890+
<McpViewsPage />,
891+
tables.database,
892+
"write",
893+
true
894+
)}
895+
/>
896+
<Route
897+
path="subject-access"
898+
element={withAuthorizationAccessCheck(
899+
<McpSubjectAccessPage />,
900+
tables.database,
901+
"write",
902+
true
903+
)}
904+
/>
905+
<Route
906+
path="check-access"
907+
element={withAuthorizationAccessCheck(
908+
<McpCheckAccessPage />,
909+
tables.database,
910+
"write",
911+
true
912+
)}
913+
/>
914+
</Route>
915+
844916
{settingsNav.submenu
845917
.filter((v) => (v as SchemaResourceType).table)
846918
.map((x) => {

src/api/services/permissions.ts

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,15 @@ export type FetchPermissionsInput = {
1717
connectionId?: string;
1818
subject?: string;
1919
action?: string;
20-
subject_type?: "playbook" | "team" | "person" | "notification" | "component";
2120
direction?: "inbound" | "outbound";
21+
subject_type?:
22+
| "playbook"
23+
| "team"
24+
| "person"
25+
| "notification"
26+
| "component"
27+
| "role"
28+
| "access_token_person";
2229
};
2330

2431
function composeQueryParamForFetchPermissions({
@@ -149,3 +156,87 @@ export function recheckPermission(id: string) {
149156
error: null
150157
});
151158
}
159+
160+
// Source marker used by the MCP Settings UI for permissions it creates/manages.
161+
export const MCP_SETTINGS_PERMISSION_SOURCE = "mcp_settings" as const;
162+
163+
export async function fetchMcpRunPermissions() {
164+
const response = await IncidentCommander.get<PermissionsSummary[] | null>(
165+
`/permissions_summary?select=*&action=eq.mcp:run&source=eq.${MCP_SETTINGS_PERMISSION_SOURCE}&deleted_at=is.null&limit=5000`
166+
);
167+
168+
return response.data ?? [];
169+
}
170+
171+
export async function fetchMcpUserPermissions() {
172+
const response = await IncidentCommander.get<PermissionsSummary[] | null>(
173+
`/permissions_summary?select=*&action=eq.mcp:use&object=eq.mcp&source=eq.${MCP_SETTINGS_PERMISSION_SOURCE}&deleted_at=is.null&limit=5000`
174+
);
175+
176+
return response.data ?? [];
177+
}
178+
179+
export type PermissionSubject = {
180+
id: string;
181+
name: string;
182+
type:
183+
| "team"
184+
| "permission_subject_group"
185+
| "person"
186+
| "role"
187+
| "access_token_person";
188+
owner?: string | null;
189+
};
190+
191+
export async function fetchPermissionSubjectsPaginated({
192+
search = "",
193+
pageIndex = 0,
194+
pageSize = 20
195+
}: {
196+
search?: string;
197+
pageIndex?: number;
198+
pageSize?: number;
199+
}) {
200+
const query = search.trim();
201+
202+
let url = "/permission_subjects?select=id,name,type,owner&order=name.asc";
203+
url += `&limit=${pageSize}&offset=${pageIndex * pageSize}`;
204+
205+
if (query) {
206+
url += `&name=ilike.*${encodeURIComponent(query)}*`;
207+
}
208+
209+
return resolvePostGrestRequestWithPagination<PermissionSubject[]>(
210+
IncidentCommander.get(url, {
211+
headers: {
212+
Prefer: "count=exact"
213+
}
214+
})
215+
);
216+
}
217+
218+
export async function fetchPermissionSubjectsByIds(ids: string[]) {
219+
if (ids.length === 0) {
220+
return [];
221+
}
222+
const response = await IncidentCommander.get<PermissionSubject[] | null>(
223+
`/permission_subjects?select=id,name,type,owner&id=in.(${ids.join(",")})&limit=${ids.length}`
224+
);
225+
return response.data ?? [];
226+
}
227+
228+
async function fetchPermissionSubjectsWithOrder(order: string) {
229+
const response = await IncidentCommander.get<PermissionSubject[] | null>(
230+
`/permission_subjects?select=id,name,type,owner&order=${order}&limit=5000`
231+
);
232+
233+
return response.data ?? [];
234+
}
235+
236+
export async function fetchPermissionSubjects() {
237+
return fetchPermissionSubjectsWithOrder("name.asc");
238+
}
239+
240+
export async function fetchAllPermissionSubjects() {
241+
return fetchPermissionSubjectsWithOrder("type.asc,name.asc");
242+
}

src/api/services/playbooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export async function getAllPlaybooksSpecs() {
2727

2828
export async function getAllPlaybookNames() {
2929
const res = await IncidentCommander.get<PlaybookNames[] | null>(
30-
`/playbook_names?select=id,name,title,icon,category&order=title.asc`
30+
`/playbook_names?select=id,name,namespace,title,icon,category,description&order=title.asc`
3131
);
3232
return res.data ?? [];
3333
}

0 commit comments

Comments
 (0)