From 578a786c1f3118c8fd2dea2f09b8be48a4cdac4a Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Sat, 27 Sep 2025 21:32:56 +0545 Subject: [PATCH 01/12] feat(permissions): add tags to form --- src/api/services/users.ts | 5 ++++- src/api/types/permissions.ts | 1 + .../ManagePermissions/Forms/PermissionForm.tsx | 9 ++++++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/api/services/users.ts b/src/api/services/users.ts index 82f897686..20ba24001 100644 --- a/src/api/services/users.ts +++ b/src/api/services/users.ts @@ -17,8 +17,11 @@ export const getPerson = (id: string) => ); export const getPersons = () => + // email=NULl filters out system user resolvePostGrestRequestWithPagination( - IncidentCommander.get(`/people?select=*&order=name.asc`) + IncidentCommander.get( + `/people?select=*&deleted_at=is.null&email=not.is.null&order=name.asc` + ) ); export const getPersonWithEmail = (email: string) => diff --git a/src/api/types/permissions.ts b/src/api/types/permissions.ts index 0b101505c..2266f17af 100644 --- a/src/api/types/permissions.ts +++ b/src/api/types/permissions.ts @@ -28,6 +28,7 @@ export type PermissionTable = { updated_at: string; until?: string; source?: string; + tags?: Record; }; export type PermissionAPIResponse = PermissionTable & { diff --git a/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx b/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx index cb37e64b6..320dfb40d 100644 --- a/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx +++ b/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx @@ -6,6 +6,7 @@ import { PermissionTable } from "@flanksource-ui/api/types/permissions"; import FormikCheckbox from "@flanksource-ui/components/Forms/Formik/FormikCheckbox"; import FormikSelectDropdown from "@flanksource-ui/components/Forms/Formik/FormikSelectDropdown"; import FormikTextArea from "@flanksource-ui/components/Forms/Formik/FormikTextArea"; +import FormikKeyValueMapField from "@flanksource-ui/components/Forms/Formik/FormikKeyValueMapField"; import CanEditResource from "@flanksource-ui/components/Settings/CanEditResource"; import { toastError, @@ -124,7 +125,8 @@ export default function PermissionForm({ person_id: data?.person_id, team_id: data?.team_id, until: data?.until, - source: data?.source || "UI" + source: data?.source || "UI", + tags: data?.tags || {} }} onSubmit={(v) => { if (!data?.id) { @@ -155,6 +157,11 @@ export default function PermissionForm({ label="Action" /> + Date: Sat, 27 Sep 2025 22:01:13 +0545 Subject: [PATCH 02/12] feat(permissions): add Role tab to subject controls Add Role tab as fourth option in permission form subject controls alongside Team, Person, and Notification. Role selection maps to subject field with subject_type set to "group" and supports admin, editor, viewer, responder, and commander roles. --- src/api/types/permissions.ts | 31 +++++++++++----- .../Forms/Formik/FormikRoleDropdown.tsx | 36 +++++++++++++++++++ .../Forms/PermissionForm.tsx | 2 ++ .../Forms/PermissionSubjectControls.tsx | 28 ++++++++++++--- 4 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 src/components/Forms/Formik/FormikRoleDropdown.tsx diff --git a/src/api/types/permissions.ts b/src/api/types/permissions.ts index 2266f17af..0aeee4507 100644 --- a/src/api/types/permissions.ts +++ b/src/api/types/permissions.ts @@ -12,23 +12,36 @@ export type PermissionTable = { deny?: boolean; object?: string; subject?: string; - subject_type?: "playbook" | "team" | "person" | "notification" | "component"; + subject_type?: + | "group" + | "playbook" + | "team" + | "person" + | "notification" + | "component"; + created_by: string; + updated_by: string; + created_at: string; + updated_at: string; + until?: string; + source?: string; + tags?: Record; + + // Resources object_selector?: Record[]; component_id?: string; config_id?: string; canary_id?: string; - playbook_id?: string; - created_by: string; connection_id?: string; + + // Deprecated fields + // These are subject fields that we do not use anymore. + // Instead we use the "subject" and "subject_type" field. + // Instead of setting person_id, we set subject = and subject_type = "person" person_id?: string; notification_id?: string; team_id?: string; - updated_by: string; - created_at: string; - updated_at: string; - until?: string; - source?: string; - tags?: Record; + playbook_id?: string; }; export type PermissionAPIResponse = PermissionTable & { diff --git a/src/components/Forms/Formik/FormikRoleDropdown.tsx b/src/components/Forms/Formik/FormikRoleDropdown.tsx new file mode 100644 index 000000000..922de4647 --- /dev/null +++ b/src/components/Forms/Formik/FormikRoleDropdown.tsx @@ -0,0 +1,36 @@ +import FormikSelectDropdown from "./FormikSelectDropdown"; + +type FormikRoleDropdownProps = { + name: string; + label?: string; + required?: boolean; + hint?: string; + className?: string; +}; + +export default function FormikRoleDropdown({ + name, + label, + required = false, + hint, + className = "flex flex-col space-y-2 py-2" +}: FormikRoleDropdownProps) { + const options = [ + { label: "Admin", value: "admin" }, + { label: "Editor", value: "editor" }, + { label: "Viewer", value: "viewer" }, + { label: "Responder", value: "responder" }, + { label: "Commander", value: "commander" } + ]; + + return ( + + ); +} diff --git a/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx b/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx index 320dfb40d..7a5d0fb73 100644 --- a/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx +++ b/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx @@ -124,6 +124,8 @@ export default function PermissionForm({ notification_id: data?.notification_id, person_id: data?.person_id, team_id: data?.team_id, + subject: data?.subject, + subject_type: data?.subject_type, until: data?.until, source: data?.source || "UI", tags: data?.tags || {} diff --git a/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx b/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx index 0a2fa6f13..71dc7bcb5 100644 --- a/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx +++ b/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx @@ -1,6 +1,7 @@ import FormikPeopleDropdown from "@flanksource-ui/components/Forms/Formik/FormikPeopleDropdown"; import FormikTeamsDropdown from "@flanksource-ui/components/Forms/Formik/FormikTeamsDropdown"; import FormikNotificationDropdown from "@flanksource-ui/components/Forms/Formik/FormikNotificationDropdown"; +import FormikRoleDropdown from "@flanksource-ui/components/Forms/Formik/FormikRoleDropdown"; import { Switch } from "@flanksource-ui/ui/FormControls/Switch"; import { useFormikContext } from "formik"; import { useEffect, useState } from "react"; @@ -11,13 +12,16 @@ export default function PermissionsSubjectControls() { const teamId = values.team_id; const personId = values.person_id; const notificationId = values.notification_id; + const subject = values.subject; + const subjectType = values.subject_type; const [switchOption, setSwitchOption] = useState< - "Team" | "Person" | "Notification" + "Team" | "Person" | "Notification" | "Role" >(() => { if (teamId) return "Team"; if (personId) return "Person"; if (notificationId) return "Notification"; + if (subjectType === "group" && subject) return "Role"; return "Team"; }); @@ -28,8 +32,10 @@ export default function PermissionsSubjectControls() { setSwitchOption("Person"); } else if (notificationId) { setSwitchOption("Notification"); + } else if (subjectType === "group" && subject) { + setSwitchOption("Role"); } - }, [teamId, personId, notificationId]); + }, [teamId, personId, notificationId, subjectType, subject]); return (
@@ -37,7 +43,7 @@ export default function PermissionsSubjectControls() {
@@ -67,6 +84,9 @@ export default function PermissionsSubjectControls() { {switchOption === "Notification" && ( )} + {switchOption === "Role" && ( + + )}
); From f68fc1cf753b600d6292f0dec6cd6f57e26307d2 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Sat, 27 Sep 2025 22:14:23 +0545 Subject: [PATCH 03/12] fix: support playbook:* action --- .../ManagePermissions/Forms/PermissionSubjectControls.tsx | 6 +++--- src/components/Permissions/PermissionsView.tsx | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx b/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx index 71dc7bcb5..eb6e0aafb 100644 --- a/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx +++ b/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx @@ -54,17 +54,17 @@ export default function PermissionsSubjectControls() { setFieldValue("person_id", undefined); setFieldValue("notification_id", undefined); setFieldValue("subject", undefined); - setFieldValue("subject_type", undefined); + setFieldValue("subject_type", "team"); } else if (v === "Person") { setFieldValue("team_id", undefined); setFieldValue("notification_id", undefined); setFieldValue("subject", undefined); - setFieldValue("subject_type", undefined); + setFieldValue("subject_type", "person"); } else if (v === "Notification") { setFieldValue("team_id", undefined); setFieldValue("person_id", undefined); setFieldValue("subject", undefined); - setFieldValue("subject_type", undefined); + setFieldValue("subject_type", "notification"); } else if (v === "Role") { setFieldValue("team_id", undefined); setFieldValue("person_id", undefined); diff --git a/src/components/Permissions/PermissionsView.tsx b/src/components/Permissions/PermissionsView.tsx index 6712e5474..05b24bf6e 100644 --- a/src/components/Permissions/PermissionsView.tsx +++ b/src/components/Permissions/PermissionsView.tsx @@ -22,7 +22,8 @@ export const permissionsActionsList: FormikSelectDropdownOption[] = [ { value: "*", label: "*" }, { value: "create,read,update,delete", label: "create,read,update,delete" }, { value: "playbook:run", label: "playbook:run" }, - { value: "playbook:approve", label: "playbook:approve" } + { value: "playbook:approve", label: "playbook:approve" }, + { value: "playbook:*", label: "playbook:*" } ]; type PermissionsViewProps = { From 723054a6e23cec9b9e8adaf7d6c50ea39bd3a348 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Sat, 27 Sep 2025 22:37:24 +0545 Subject: [PATCH 04/12] fix: subject controls --- .../Forms/PermissionSubjectControls.tsx | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx b/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx index eb6e0aafb..d3bcbcf62 100644 --- a/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx +++ b/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx @@ -18,9 +18,10 @@ export default function PermissionsSubjectControls() { const [switchOption, setSwitchOption] = useState< "Team" | "Person" | "Notification" | "Role" >(() => { - if (teamId) return "Team"; - if (personId) return "Person"; - if (notificationId) return "Notification"; + if (teamId || (subjectType === "team" && subject)) return "Team"; + if (personId || (subjectType === "person" && subject)) return "Person"; + if (notificationId || (subjectType === "notification" && subject)) + return "Notification"; if (subjectType === "group" && subject) return "Role"; return "Team"; }); @@ -28,7 +29,7 @@ export default function PermissionsSubjectControls() { useEffect(() => { if (teamId) { setSwitchOption("Team"); - } else if (personId) { + } else if (personId || (subjectType === "person" && subject)) { setSwitchOption("Person"); } else if (notificationId) { setSwitchOption("Notification"); @@ -50,25 +51,18 @@ export default function PermissionsSubjectControls() { value={switchOption} onChange={(v) => { setSwitchOption(v); + + setFieldValue("person_id", undefined); + setFieldValue("notification_id", undefined); + setFieldValue("team_id", undefined); + if (v === "Team") { - setFieldValue("person_id", undefined); - setFieldValue("notification_id", undefined); - setFieldValue("subject", undefined); setFieldValue("subject_type", "team"); } else if (v === "Person") { - setFieldValue("team_id", undefined); - setFieldValue("notification_id", undefined); - setFieldValue("subject", undefined); setFieldValue("subject_type", "person"); } else if (v === "Notification") { - setFieldValue("team_id", undefined); - setFieldValue("person_id", undefined); - setFieldValue("subject", undefined); setFieldValue("subject_type", "notification"); } else if (v === "Role") { - setFieldValue("team_id", undefined); - setFieldValue("person_id", undefined); - setFieldValue("notification_id", undefined); setFieldValue("subject_type", "group"); } }} @@ -76,13 +70,13 @@ export default function PermissionsSubjectControls() {
{switchOption === "Team" && ( - + )} {switchOption === "Person" && ( - + )} {switchOption === "Notification" && ( - + )} {switchOption === "Role" && ( From 1c66e68671c53539e05986274c4fe3006a0fe556 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Sat, 27 Sep 2025 22:42:04 +0545 Subject: [PATCH 05/12] feat(permissions): add playbook support to subject controls Add playbook as a new subject type option in PermissionSubjectControls: - Import FormikPlaybooksDropdown component - Add "Playbook" to switch options and type union - Handle playbook selection in state initialization and effects - Set subject_type to "playbook" when playbook is selected - Store selected playbook ID in subject field --- .../Forms/PermissionSubjectControls.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx b/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx index d3bcbcf62..b6b582d4e 100644 --- a/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx +++ b/src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx @@ -2,6 +2,7 @@ import FormikPeopleDropdown from "@flanksource-ui/components/Forms/Formik/Formik import FormikTeamsDropdown from "@flanksource-ui/components/Forms/Formik/FormikTeamsDropdown"; import FormikNotificationDropdown from "@flanksource-ui/components/Forms/Formik/FormikNotificationDropdown"; import FormikRoleDropdown from "@flanksource-ui/components/Forms/Formik/FormikRoleDropdown"; +import FormikPlaybooksDropdown from "@flanksource-ui/components/Forms/Formik/FormikPlaybooksDropdown"; import { Switch } from "@flanksource-ui/ui/FormControls/Switch"; import { useFormikContext } from "formik"; import { useEffect, useState } from "react"; @@ -16,13 +17,14 @@ export default function PermissionsSubjectControls() { const subjectType = values.subject_type; const [switchOption, setSwitchOption] = useState< - "Team" | "Person" | "Notification" | "Role" + "Team" | "Person" | "Notification" | "Role" | "Playbook" >(() => { if (teamId || (subjectType === "team" && subject)) return "Team"; if (personId || (subjectType === "person" && subject)) return "Person"; if (notificationId || (subjectType === "notification" && subject)) return "Notification"; if (subjectType === "group" && subject) return "Role"; + if (subjectType === "playbook" && subject) return "Playbook"; return "Team"; }); @@ -35,6 +37,8 @@ export default function PermissionsSubjectControls() { setSwitchOption("Notification"); } else if (subjectType === "group" && subject) { setSwitchOption("Role"); + } else if (subjectType === "playbook" && subject) { + setSwitchOption("Playbook"); } }, [teamId, personId, notificationId, subjectType, subject]); @@ -44,7 +48,7 @@ export default function PermissionsSubjectControls() {
{ setSwitchOption(v); + // These are old deprecated values that must never be set anymore. setFieldValue("person_id", undefined); setFieldValue("notification_id", undefined); setFieldValue("team_id", undefined); @@ -64,6 +69,8 @@ export default function PermissionsSubjectControls() { setFieldValue("subject_type", "notification"); } else if (v === "Role") { setFieldValue("subject_type", "group"); + } else if (v === "Playbook") { + setFieldValue("subject_type", "playbook"); } }} /> @@ -81,6 +88,9 @@ export default function PermissionsSubjectControls() { {switchOption === "Role" && ( )} + {switchOption === "Playbook" && ( + + )}
); From f4a6eedaedb8a33dd96fbb339dcbc612ebc69dd6 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Sat, 27 Sep 2025 22:48:40 +0545 Subject: [PATCH 06/12] fix(permission): Refresh --- src/components/Permissions/PermissionsView.tsx | 10 +++++++++- src/pages/Settings/PermissionsPage.tsx | 14 ++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/components/Permissions/PermissionsView.tsx b/src/components/Permissions/PermissionsView.tsx index 05b24bf6e..a2ec34189 100644 --- a/src/components/Permissions/PermissionsView.tsx +++ b/src/components/Permissions/PermissionsView.tsx @@ -32,6 +32,7 @@ type PermissionsViewProps = { hideResourceColumn?: boolean; newPermissionData?: Partial; showAddPermission?: boolean; + onRefetch?: (refetch: () => void) => void; }; export default function PermissionsView({ @@ -39,7 +40,8 @@ export default function PermissionsView({ setIsLoading = () => {}, hideResourceColumn = false, newPermissionData, - showAddPermission = false + showAddPermission = false, + onRefetch }: PermissionsViewProps) { const [selectedPermission, setSelectedPermission] = useState(); @@ -67,6 +69,12 @@ export default function PermissionsView({ setIsLoading(isLoading); }, [isLoading, setIsLoading]); + useEffect(() => { + if (onRefetch) { + onRefetch(refetch); + } + }, [onRefetch, refetch]); + const totalEntries = data?.totalEntries || 0; const pageCount = totalEntries ? Math.ceil(totalEntries / pageSize) : 1; const permissions = data?.data || []; diff --git a/src/pages/Settings/PermissionsPage.tsx b/src/pages/Settings/PermissionsPage.tsx index c601e9e95..48d90d79e 100644 --- a/src/pages/Settings/PermissionsPage.tsx +++ b/src/pages/Settings/PermissionsPage.tsx @@ -8,12 +8,11 @@ import { } from "@flanksource-ui/ui/BreadcrumbNav"; import { Head } from "@flanksource-ui/ui/Head"; import { SearchLayout } from "@flanksource-ui/ui/Layout/SearchLayout"; -import { useQueryClient } from "@tanstack/react-query"; -import { useState } from "react"; +import { useState, useRef } from "react"; export function PermissionsPage() { const [isLoading, setIsLoading] = useState(false); - const client = useQueryClient(); + const refetchFunctionRef = useRef<(() => void) | null>(null); return ( <> @@ -38,11 +37,7 @@ export function PermissionsPage() { ]} /> } - onRefresh={() => - client.refetchQueries({ - queryKey: ["permissions"] - }) - } + onRefresh={() => refetchFunctionRef.current?.()} contentClass="p-0 h-full" loading={isLoading} > @@ -51,6 +46,9 @@ export function PermissionsPage() { { + refetchFunctionRef.current = refetch; + }} /> From cd80d0e7cc3383da079457f68c0d2de6e8ce3a83 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Sat, 27 Sep 2025 22:55:07 +0545 Subject: [PATCH 07/12] feat(permissions): move RLS payload display to Resource column - Remove RLS payload (tags/agents) from Subject column - Add individual badges for each tag and agent in Resource column - Display tags as "key: value" badges in blue - Display agents as "agent: name" badges in gray - Use inline layout for compact display alongside resource info --- .../Permissions/PermissionsTable.tsx | 113 ++++++++++++------ 1 file changed, 77 insertions(+), 36 deletions(-) diff --git a/src/components/Permissions/PermissionsTable.tsx b/src/components/Permissions/PermissionsTable.tsx index ed06247f0..553821160 100644 --- a/src/components/Permissions/PermissionsTable.tsx +++ b/src/components/Permissions/PermissionsTable.tsx @@ -14,6 +14,10 @@ import { BsBan } from "react-icons/bs"; import { Link } from "react-router-dom"; import { Badge } from "@flanksource-ui/ui/Badge/Badge"; +const formatTagText = (key: string, value: string): string => { + return `${key}: ${value}`; +}; + const permissionsTableColumns: MRT_ColumnDef[] = [ { header: "Subject", @@ -21,18 +25,6 @@ const permissionsTableColumns: MRT_ColumnDef[] = [ Cell: ({ row }) => { const { team, group, person, subject, notification, playbook } = row.original; - const { tags, agents } = row.original; - const rlsFilter = []; - - if (tags && Object.keys(tags).length > 0) { - rlsFilter.push(tags); - } - - if (agents && agents.length > 0) { - rlsFilter.push({ agents: agents }); - } - - const rlsPayload = rlsFilter.length > 0 ? JSON.stringify(rlsFilter) : ""; if (group) { const groupName = group.name || subject; @@ -43,7 +35,6 @@ const permissionsTableColumns: MRT_ColumnDef[] = [ {groupName} {/* Add link to permission group when we have a permission group page */} - {rlsPayload && } ); } @@ -53,7 +44,6 @@ const permissionsTableColumns: MRT_ColumnDef[] = [
{person.name} - {rlsPayload && }
); } @@ -63,7 +53,6 @@ const permissionsTableColumns: MRT_ColumnDef[] = [
{team.name} - {rlsPayload && }
); } @@ -81,7 +70,6 @@ const permissionsTableColumns: MRT_ColumnDef[] = [ notification.name} - {rlsPayload && } ); } @@ -100,7 +88,6 @@ const permissionsTableColumns: MRT_ColumnDef[] = [ - {rlsPayload && } ); } @@ -121,36 +108,90 @@ const permissionsTableColumns: MRT_ColumnDef[] = [ const connection = row.original.connection; const object = row.original.object; const objectSelector = row.original.object_selector; + const { tags, agents } = row.original; + + const renderRlsBadges = (): JSX.Element[] => { + const badges: JSX.Element[] = []; + + // Add tag badges + if (tags && Object.keys(tags).length > 0) { + Object.entries(tags).forEach(([key, value]) => { + badges.push( + + ); + }); + } + + // Add agent badges + if (agents && agents.length > 0) { + agents.forEach((agent, index) => { + badges.push( + + ); + }); + } + + return badges; + }; + + const rlsBadges = renderRlsBadges(); if (objectSelector) { return ( - - {JSON.stringify(objectSelector)} - +
+ + {JSON.stringify(objectSelector)} + + {rlsBadges.length > 0 && ( +
{rlsBadges}
+ )} +
); } if (object) { - return permissionObjectList.find((o) => o.value === object)?.label; + return ( +
+ + {permissionObjectList.find((o) => o.value === object)?.label} + + {rlsBadges.length > 0 && ( +
{rlsBadges}
+ )} +
+ ); } return ( -
- {config && } - {/* {check && } */} - {playbook && } - {component && ( - +
+
+ {config && } + {/* {check && } */} + {playbook && } + {component && ( + + )} + {connection && } +
+ {rlsBadges.length > 0 && ( +
{rlsBadges}
)} - {connection && }
); } From 06188d69873bb36829aca46750dd7a12eb53858e Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Wed, 1 Oct 2025 12:08:02 +0545 Subject: [PATCH 08/12] feat: add guest role * Move subject to the top --- src/components/Forms/Formik/FormikRoleDropdown.tsx | 1 + .../Permissions/ManagePermissions/Forms/PermissionForm.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Forms/Formik/FormikRoleDropdown.tsx b/src/components/Forms/Formik/FormikRoleDropdown.tsx index 922de4647..946517f3a 100644 --- a/src/components/Forms/Formik/FormikRoleDropdown.tsx +++ b/src/components/Forms/Formik/FormikRoleDropdown.tsx @@ -18,6 +18,7 @@ export default function FormikRoleDropdown({ const options = [ { label: "Admin", value: "admin" }, { label: "Editor", value: "editor" }, + { label: "Guest", value: "guest" }, { label: "Viewer", value: "viewer" }, { label: "Responder", value: "responder" }, { label: "Commander", value: "commander" } diff --git a/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx b/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx index 7a5d0fb73..58361b8bb 100644 --- a/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx +++ b/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx @@ -144,6 +144,7 @@ export default function PermissionForm({ >
+ {isResourceIdProvided ? (
@@ -152,7 +153,6 @@ export default function PermissionForm({ ) : ( )} - Date: Wed, 1 Oct 2025 12:08:42 +0545 Subject: [PATCH 09/12] feat: agents field --- src/api/types/permissions.ts | 1 + .../Forms/PermissionForm.tsx | 36 +++++++++++++++---- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/api/types/permissions.ts b/src/api/types/permissions.ts index 0aeee4507..0d269e374 100644 --- a/src/api/types/permissions.ts +++ b/src/api/types/permissions.ts @@ -26,6 +26,7 @@ export type PermissionTable = { until?: string; source?: string; tags?: Record; + agents?: string[]; // Resources object_selector?: Record[]; diff --git a/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx b/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx index 58361b8bb..5a37b6294 100644 --- a/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx +++ b/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx @@ -28,6 +28,7 @@ import DeletePermission from "./DeletePermission"; import FormikPermissionSelectResourceFields from "./FormikPermissionSelectResourceFields"; import PermissionResource from "./PermissionResource"; import PermissionsSubjectControls from "./PermissionSubjectControls"; +import { useAllAgentNamesQuery } from "../../../../api/query-hooks"; type PermissionFormProps = { onClose: () => void; @@ -54,6 +55,17 @@ export default function PermissionForm({ const { user } = useUser(); + const { data: agents } = useAllAgentNamesQuery({}); + + const agentOptions = useMemo( + () => + (agents || []).map((agent) => ({ + label: agent.name || agent.id, + value: agent.id + })), + [agents] + ); + const { isLoading: adding, mutate: add } = useMutation({ mutationFn: async (data: PermissionTable) => { const res = await addPermission({ @@ -128,7 +140,8 @@ export default function PermissionForm({ subject_type: data?.subject_type, until: data?.until, source: data?.source || "UI", - tags: data?.tags || {} + tags: data?.tags || {}, + agents: data?.agents || [] }} onSubmit={(v) => { if (!data?.id) { @@ -159,11 +172,22 @@ export default function PermissionForm({ label="Action" /> - +
+
+ + +
+
Date: Wed, 1 Oct 2025 12:14:34 +0545 Subject: [PATCH 10/12] feat: hint for playbook and connection resource --- .../Forms/Formik/FormikConnectionField.tsx | 2 +- .../Forms/FormikPermissionSelectResourceFields.tsx | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/Forms/Formik/FormikConnectionField.tsx b/src/components/Forms/Formik/FormikConnectionField.tsx index afe46e895..33a3d6634 100644 --- a/src/components/Forms/Formik/FormikConnectionField.tsx +++ b/src/components/Forms/Formik/FormikConnectionField.tsx @@ -45,7 +45,7 @@ export default function FormikConnectionField({ return ( + )} {switchOption === "Canary" && ( @@ -86,7 +90,11 @@ export default function FormikPermissionSelectResourceFields() { )} {switchOption === "Connection" && ( - + )} {switchOption === "Global" && ( From d85c7fc64d86c2d666f23d71554d817f2f2b3c59 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Wed, 1 Oct 2025 12:18:17 +0545 Subject: [PATCH 11/12] feat(permissions): dynamic actions based on resource type - Action dropdown now hidden until resource is selected - Playbook resources show playbook-specific actions (run, approve, *) - Other resources (catalog, component, connection, canary, global) show common actions only --- .../Forms/PermissionForm.tsx | 41 +++++++++++++++---- .../Permissions/PermissionsView.tsx | 37 +++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx b/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx index 5a37b6294..2c007d607 100644 --- a/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx +++ b/src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx @@ -19,11 +19,11 @@ import { Modal } from "@flanksource-ui/ui/Modal"; import { useMutation } from "@tanstack/react-query"; import { AxiosError } from "axios"; import clsx from "clsx"; -import { Form, Formik } from "formik"; +import { Form, Formik, useFormikContext } from "formik"; import { useMemo } from "react"; import { FaSpinner } from "react-icons/fa"; import { AuthorizationAccessCheck } from "../../AuthorizationAccessCheck"; -import { permissionsActionsList } from "../../PermissionsView"; +import { getActionsForResourceType, ResourceType } from "../../PermissionsView"; import DeletePermission from "./DeletePermission"; import FormikPermissionSelectResourceFields from "./FormikPermissionSelectResourceFields"; import PermissionResource from "./PermissionResource"; @@ -36,6 +36,37 @@ type PermissionFormProps = { data?: Partial; }; +function PermissionActionDropdown() { + const { values } = useFormikContext>(); + + const resourceType = useMemo(() => { + if (values.playbook_id) return "playbook"; + if (values.config_id) return "catalog"; + if (values.component_id) return "component"; + if (values.connection_id) return "connection"; + if (values.canary_id) return "canary"; + if (values.object) return "global"; + return undefined; + }, [values]); + + const availableActions = useMemo( + () => getActionsForResourceType(resourceType), + [resourceType] + ); + + if (!resourceType) { + return null; + } + + return ( + + ); +} + export default function PermissionForm({ onClose, isOpen = false, @@ -166,11 +197,7 @@ export default function PermissionForm({ ) : ( )} - +
diff --git a/src/components/Permissions/PermissionsView.tsx b/src/components/Permissions/PermissionsView.tsx index a2ec34189..a00faadc2 100644 --- a/src/components/Permissions/PermissionsView.tsx +++ b/src/components/Permissions/PermissionsView.tsx @@ -26,6 +26,43 @@ export const permissionsActionsList: FormikSelectDropdownOption[] = [ { value: "playbook:*", label: "playbook:*" } ]; +const commonActions: FormikSelectDropdownOption[] = [ + { value: "read", label: "read" }, + { value: "update", label: "update" }, + { value: "create", label: "create" }, + { value: "delete", label: "delete" }, + { value: "*", label: "*" }, + { value: "create,read,update,delete", label: "create,read,update,delete" } +]; + +const playbookSpecificActions: FormikSelectDropdownOption[] = [ + { value: "playbook:run", label: "playbook:run" }, + { value: "playbook:approve", label: "playbook:approve" }, + { value: "playbook:*", label: "playbook:*" } +]; + +export type ResourceType = + | "catalog" + | "component" + | "playbook" + | "connection" + | "canary" + | "global"; + +export function getActionsForResourceType( + resourceType?: ResourceType +): FormikSelectDropdownOption[] { + if (!resourceType) { + return []; + } + + if (resourceType === "playbook") { + return [...commonActions, ...playbookSpecificActions]; + } + + return commonActions; +} + type PermissionsViewProps = { permissionRequest: FetchPermissionsInput; setIsLoading?: (isLoading: boolean) => void; From 800b094d54f875cfb9f281a2e775fa453635a6c7 Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Wed, 1 Oct 2025 12:27:08 +0545 Subject: [PATCH 12/12] feat(permissions): use FormikResourceSelectorDropdown for connections and playbooks Unifies resource selection by using FormikResourceSelectorDropdown for both connection and playbook resources instead of their dedicated components, providing consistent filtering and search functionality across all resource types. --- src/api/services/search.ts | 4 + .../Formik/FormikResourceSelectorDropdown.tsx | 80 ++++++++++++++++++- .../FormikPermissionSelectResourceFields.tsx | 10 +-- 3 files changed, 87 insertions(+), 7 deletions(-) diff --git a/src/api/services/search.ts b/src/api/services/search.ts index a24376b43..b164f181c 100644 --- a/src/api/services/search.ts +++ b/src/api/services/search.ts @@ -5,6 +5,8 @@ export type SearchResourcesRequest = { checks?: PlaybookResourceSelector[]; components?: PlaybookResourceSelector[]; configs?: PlaybookResourceSelector[]; + connections?: PlaybookResourceSelector[]; + playbooks?: PlaybookResourceSelector[]; }; type SearchedResource = { @@ -22,6 +24,8 @@ type SelectedResources = { configs: SearchedResource[]; checks: SearchedResource[]; components: SearchedResource[]; + connections: SearchedResource[]; + playbooks: SearchedResource[]; }; export async function searchResources(input: SearchResourcesRequest) { diff --git a/src/components/Forms/Formik/FormikResourceSelectorDropdown.tsx b/src/components/Forms/Formik/FormikResourceSelectorDropdown.tsx index e6489c927..6f64daf42 100644 --- a/src/components/Forms/Formik/FormikResourceSelectorDropdown.tsx +++ b/src/components/Forms/Formik/FormikResourceSelectorDropdown.tsx @@ -22,6 +22,8 @@ type FormikConfigsDropdownProps = { configResourceSelector?: PlaybookResourceSelector[]; componentResourceSelector?: PlaybookResourceSelector[]; checkResourceSelector?: PlaybookResourceSelector[]; + connectionResourceSelector?: PlaybookResourceSelector[]; + playbookResourceSelector?: PlaybookResourceSelector[]; className?: string; }; @@ -33,6 +35,8 @@ export default function FormikResourceSelectorDropdown({ configResourceSelector, componentResourceSelector, checkResourceSelector, + connectionResourceSelector, + playbookResourceSelector, className = "flex flex-col space-y-2 py-2" }: FormikConfigsDropdownProps) { const [inputText, setInputText] = useState(""); @@ -81,12 +85,32 @@ export default function FormikResourceSelectorDropdown({ agent: r.agent || "all" })) ] + : undefined, + connections: connectionResourceSelector + ? [ + ...connectionResourceSelector.map((r) => ({ + ...r, + search: searchText, + name: r.name || "*" + })) + ] + : undefined, + playbooks: playbookResourceSelector + ? [ + ...playbookResourceSelector.map((r) => ({ + ...r, + search: searchText, + name: r.name || "*" + })) + ] : undefined }), [ configResourceSelector, componentResourceSelector, checkResourceSelector, + connectionResourceSelector, + playbookResourceSelector, searchText ] ); @@ -101,7 +125,9 @@ export default function FormikResourceSelectorDropdown({ enabled: configResourceSelector !== undefined || componentResourceSelector !== undefined || - checkResourceSelector !== undefined, // || (field.value === undefined && field.value === "" && field.value === null), + checkResourceSelector !== undefined || + connectionResourceSelector !== undefined || + playbookResourceSelector !== undefined, // || (field.value === undefined && field.value === "" && field.value === null), select: (data) => { if (data?.checks) { return data.checks.map( @@ -159,6 +185,58 @@ export default function FormikResourceSelectorDropdown({ } as FormikSelectDropdownOption; }); } + if (data?.connections) { + return data.connections.map((connection) => { + const tags = Object.values(connection.tags ?? {}).map( + (value) => value + ); + + return { + icon: ( + + ), + value: connection.id, + search: connection.name, + label: ( +
+ {connection.name} + + {connection.type.split("::").at(-1)?.toLocaleLowerCase()} + + {tags.map((tag) => ( + + {tag} + + ))} +
+ ) + } as FormikSelectDropdownOption; + }); + } + if (data?.playbooks) { + return data.playbooks.map((playbook) => { + return { + icon: ( + + ), + value: playbook.id, + search: playbook.name, + label: ( +
+ {playbook.name} +
+ ) + } as FormikSelectDropdownOption; + }); + } }, keepPreviousData: true, staleTime: 0, diff --git a/src/components/Permissions/ManagePermissions/Forms/FormikPermissionSelectResourceFields.tsx b/src/components/Permissions/ManagePermissions/Forms/FormikPermissionSelectResourceFields.tsx index 69dcf6b3c..67d322b95 100644 --- a/src/components/Permissions/ManagePermissions/Forms/FormikPermissionSelectResourceFields.tsx +++ b/src/components/Permissions/ManagePermissions/Forms/FormikPermissionSelectResourceFields.tsx @@ -1,6 +1,4 @@ import FormikCanaryDropdown from "@flanksource-ui/components/Forms/Formik/FormikCanaryDropdown"; -import FormikConnectionField from "@flanksource-ui/components/Forms/Formik/FormikConnectionField"; -import FormikPlaybooksDropdown from "@flanksource-ui/components/Forms/Formik/FormikPlaybooksDropdown"; import FormikResourceSelectorDropdown from "@flanksource-ui/components/Forms/Formik/FormikResourceSelectorDropdown"; import FormikSelectDropdown from "@flanksource-ui/components/Forms/Formik/FormikSelectDropdown"; import { Switch } from "@flanksource-ui/ui/FormControls/Switch"; @@ -78,10 +76,10 @@ export default function FormikPermissionSelectResourceFields() { )} {switchOption === "Playbook" && ( - )} @@ -90,10 +88,10 @@ export default function FormikPermissionSelectResourceFields() { )} {switchOption === "Connection" && ( - )}