Skip to content

Commit df62e86

Browse files
authored
feat: permission form updates (#2648)
* feat: add creator avatar to permissions table - Join with people table to fetch created_by user info - Display avatar in Created By column - Handle missing creator gracefully * feat: make permission form read-only for CRD-managed permissions - Add read-only banner when source is KubernetesCRD - Disable all form fields with pointer-events-none and opacity - Add disabled prop to PermissionSubjectControls - Display linked CRD in footer via CanEditResource - Only show delete button for UI-created permissions * refactor: remove tags and agents fields from permissions - Remove tags and agents from PermissionTable type - Remove tags and agents from PermissionsSummary type - Remove badge rendering logic from permissions table - Simplify Resource column display * fix: remove unsupported disabled prop from Switch component * fix: remove disabled prop from Formik dropdowns that don't support it
1 parent fdfd7a0 commit df62e86

File tree

5 files changed

+81
-105
lines changed

5 files changed

+81
-105
lines changed

src/api/services/permissions.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { IncidentCommander } from "../axios";
22
import { resolvePostGrestRequestWithPagination } from "../resolve";
33
import { PermissionsSummary, PermissionTable } from "../types/permissions";
4+
import { AVATAR_INFO } from "@flanksource-ui/constants";
45

56
export type FetchPermissionsInput = {
67
componentId?: string;
@@ -68,11 +69,11 @@ export function fetchPermissions(
6869
}
6970
) {
7071
const queryParam = composeQueryParamForFetchPermissions(input);
71-
const selectFields = ["*"];
72+
const selectFields = `*,created_by(${AVATAR_INFO})`;
7273

7374
const { pageSize, pageIndex } = pagination;
7475

75-
const url = `/permissions_summary?${queryParam}&select=${selectFields.join(",")}&deleted_at=is.null&limit=${pageSize}&offset=${pageIndex * pageSize}`;
76+
const url = `/permissions_summary?${queryParam}&select=${selectFields}&deleted_at=is.null&limit=${pageSize}&offset=${pageIndex * pageSize}`;
7677
return resolvePostGrestRequestWithPagination(
7778
IncidentCommander.get<PermissionsSummary[]>(url, {
7879
headers: {

src/api/types/permissions.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,6 @@ export type PermissionTable = {
3232
updated_at: string;
3333
until?: string;
3434
source?: string;
35-
tags?: Record<string, string>;
36-
agents?: string[];
3735

3836
// Resources
3937
object?: PermissionGlobalObject;
@@ -68,17 +66,16 @@ export type PermissionsSummary = PermissionTable & {
6866
group: any;
6967
subject: string;
7068
person: User;
71-
createdBy: User;
72-
tags: Record<string, string> | null;
73-
agents: string[] | null;
69+
created_by?: User;
7470

7571
// These represent global objects
7672
object: PermissionGlobalObject;
7773

7874
// These represent object selectors per type
7975
object_selector?: PermissionObjectSelector;
8076

81-
// These are objects that are specifically chosen
77+
// These are objects that are specifically chosen (from the dropdown).
78+
// We store their ID.
8279
config_object: Pick<ConfigItem, "id" | "name" | "type" | "config_class">;
8380
playbook_object: Pick<PlaybookSpec, "id" | "name" | "icon">;
8481
connection_object: Pick<Connection, "id" | "name" | "type">;

src/components/Permissions/ManagePermissions/Forms/PermissionForm.tsx

Lines changed: 62 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { Button } from "@flanksource-ui/ui/Buttons/Button";
1818
import { Modal } from "@flanksource-ui/ui/Modal";
1919
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
2020
import { AxiosError } from "axios";
21-
import clsx from "clsx";
2221
import { Form, Formik, useFormikContext } from "formik";
2322
import { useMemo } from "react";
2423
import { FaSpinner } from "react-icons/fa";
@@ -78,22 +77,45 @@ function PermissionFormContent({
7877

7978
return (
8079
<div className="flex flex-col gap-3 p-4">
81-
<PermissionsSubjectControls />
82-
{isResourceIdProvided && isReadOnly ? (
83-
<div className="flex flex-col gap-2">
84-
<label className="text-sm font-semibold">Resource</label>
85-
<PermissionResource />
80+
{isReadOnly && (
81+
<div className="rounded-md border border-yellow-300 bg-yellow-50 p-3 text-sm text-yellow-900">
82+
<p className="font-medium">
83+
Read-Only Mode: This resource is managed by Kubernetes CRD and
84+
cannot be edited from the UI.
85+
</p>
8686
</div>
87-
) : (
88-
<FormikPermissionSelectResourceFields />
8987
)}
90-
<PermissionActionDropdown isDisabled={isReadOnly} />
91-
<FormikCheckbox name="deny" label="Deny" disabled={isReadOnly} />
92-
<FormikTextArea
93-
name="description"
94-
label="Description"
95-
disabled={isReadOnly}
96-
/>
88+
89+
<div className={isReadOnly ? "pointer-events-none opacity-60" : ""}>
90+
<PermissionsSubjectControls disabled={isReadOnly} />
91+
</div>
92+
93+
<div className={isReadOnly ? "pointer-events-none opacity-60" : ""}>
94+
{isResourceIdProvided && isReadOnly ? (
95+
<div className="flex flex-col gap-2">
96+
<label className="text-sm font-semibold">Resource</label>
97+
<PermissionResource />
98+
</div>
99+
) : (
100+
<FormikPermissionSelectResourceFields />
101+
)}
102+
</div>
103+
104+
<div className={isReadOnly ? "pointer-events-none opacity-60" : ""}>
105+
<PermissionActionDropdown isDisabled={isReadOnly} />
106+
</div>
107+
108+
<div className={isReadOnly ? "pointer-events-none opacity-60" : ""}>
109+
<FormikCheckbox name="deny" label="Deny" disabled={isReadOnly} />
110+
</div>
111+
112+
<div className={isReadOnly ? "pointer-events-none opacity-60" : ""}>
113+
<FormikTextArea
114+
name="description"
115+
label="Description"
116+
disabled={isReadOnly}
117+
/>
118+
</div>
97119
</div>
98120
);
99121
}
@@ -247,41 +269,36 @@ export default function PermissionForm({
247269
id={permissionData?.id}
248270
resourceType={"permissions"}
249271
source={permissionData?.source}
250-
className="flex items-center bg-gray-100 px-5 py-4"
272+
className="flex items-center justify-between bg-gray-100 px-5 py-4"
251273
>
252-
<AuthorizationAccessCheck
253-
resource={tables.permissions}
254-
action="write"
255-
>
256-
<div
257-
className={clsx(
258-
"flex items-center bg-gray-100 px-5 py-4",
259-
permissionData?.id ? "justify-between" : "justify-end"
260-
)}
261-
>
262-
{permissionData?.id && (
274+
<div>
275+
{permissionData?.id && permissionData.source === "UI" && (
276+
<AuthorizationAccessCheck
277+
resource={tables.permissions}
278+
action="write"
279+
>
263280
<DeletePermission
264281
permissionId={permissionData.id}
265282
onDeleted={onClose}
266283
/>
267-
)}
268-
<Button
269-
icon={
270-
isLoading ? (
271-
<FaSpinner className="animate-spin" />
272-
) : undefined
273-
}
274-
type="submit"
275-
text={
276-
permissionData?.id
277-
? "Save"
278-
: isLoading
279-
? "Saving ..."
280-
: "Save"
281-
}
282-
className="btn-primary"
283-
/>
284-
</div>
284+
</AuthorizationAccessCheck>
285+
)}
286+
</div>
287+
<AuthorizationAccessCheck
288+
resource={tables.permissions}
289+
action="write"
290+
>
291+
<Button
292+
type="submit"
293+
text={permissionData?.id ? "Save" : "Create"}
294+
className="btn-primary"
295+
icon={
296+
isLoading ? (
297+
<FaSpinner className="animate-spin" />
298+
) : undefined
299+
}
300+
disabled={isLoading}
301+
/>
285302
</AuthorizationAccessCheck>
286303
</CanEditResource>
287304
</Form>

src/components/Permissions/ManagePermissions/Forms/PermissionSubjectControls.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ const SWITCH_OPTION_TO_SUBJECT_TYPE = {
1515
Playbook: "playbook"
1616
} as const;
1717

18-
export default function PermissionsSubjectControls() {
18+
type PermissionsSubjectControlsProps = {
19+
disabled?: boolean;
20+
};
21+
22+
export default function PermissionsSubjectControls({
23+
disabled = false
24+
}: PermissionsSubjectControlsProps) {
1925
const { values, setFieldValue } = useFormikContext<Record<string, any>>();
2026

2127
const teamId = values.team_id;
@@ -71,6 +77,7 @@ export default function PermissionsSubjectControls() {
7177
defaultValue="Go Template"
7278
value={switchOption}
7379
onChange={(v) => {
80+
if (disabled) return;
7481
setSwitchOption(v);
7582
setFieldValue("subject_type", SWITCH_OPTION_TO_SUBJECT_TYPE[v]);
7683

src/components/Permissions/PermissionsTable.tsx

Lines changed: 5 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,8 @@ import { permissionObjectList } from "./ManagePermissions/Forms/FormikPermission
1212
import { permissionsActionsList } from "./PermissionsView";
1313
import { BsBan } from "react-icons/bs";
1414
import { Link } from "react-router-dom";
15-
import { Badge } from "@flanksource-ui/ui/Badge/Badge";
1615
import CRDSource from "../Settings/CRDSource";
1716

18-
const formatTagText = (key: string, value: string): string => {
19-
return `${key}: ${value}`;
20-
};
21-
2217
const permissionsTableColumns: MRT_ColumnDef<PermissionsSummary>[] = [
2318
{
2419
header: "Subject",
@@ -108,41 +103,6 @@ const permissionsTableColumns: MRT_ColumnDef<PermissionsSummary>[] = [
108103
const connection = row.original.connection_object;
109104
const object = row.original.object;
110105
const objectSelector = row.original.object_selector;
111-
const { tags, agents } = row.original;
112-
113-
const renderRlsBadges = (): JSX.Element[] => {
114-
const badges: JSX.Element[] = [];
115-
116-
// Add tag badges
117-
if (tags && Object.keys(tags).length > 0) {
118-
Object.entries(tags).forEach(([key, value]) => {
119-
badges.push(
120-
<Badge
121-
key={`tag-${key}`}
122-
text={formatTagText(key, value)}
123-
color="blue"
124-
/>
125-
);
126-
});
127-
}
128-
129-
// Add agent badges
130-
if (agents && agents.length > 0) {
131-
agents.forEach((agent, index) => {
132-
badges.push(
133-
<Badge
134-
key={`agent-${index}`}
135-
text={`agent: ${agent}`}
136-
color="gray"
137-
/>
138-
);
139-
});
140-
}
141-
142-
return badges;
143-
};
144-
145-
const rlsBadges = renderRlsBadges();
146106

147107
if (objectSelector) {
148108
return (
@@ -153,9 +113,6 @@ const permissionsTableColumns: MRT_ColumnDef<PermissionsSummary>[] = [
153113
>
154114
{JSON.stringify(objectSelector)}
155115
</span>
156-
{rlsBadges.length > 0 && (
157-
<div className="flex flex-wrap gap-1">{rlsBadges}</div>
158-
)}
159116
</div>
160117
);
161118
}
@@ -166,9 +123,6 @@ const permissionsTableColumns: MRT_ColumnDef<PermissionsSummary>[] = [
166123
<span>
167124
{permissionObjectList.find((o) => o.value === object)?.label}
168125
</span>
169-
{rlsBadges.length > 0 && (
170-
<div className="flex flex-wrap gap-1">{rlsBadges}</div>
171-
)}
172126
</div>
173127
);
174128
}
@@ -216,10 +170,6 @@ const permissionsTableColumns: MRT_ColumnDef<PermissionsSummary>[] = [
216170
</div>
217171
)}
218172
</div>
219-
220-
{rlsBadges.length > 0 && (
221-
<div className="flex flex-wrap gap-1">{rlsBadges}</div>
222-
)}
223173
</div>
224174
);
225175
}
@@ -280,14 +230,18 @@ const permissionsTableColumns: MRT_ColumnDef<PermissionsSummary>[] = [
280230
header: "Created By",
281231
size: 40,
282232
Cell: ({ row }) => {
283-
const createdBy = row.original.createdBy;
233+
const createdBy = row.original.created_by;
284234
const source = row.original.source;
285235

286236
if (source?.toLowerCase() === "KubernetesCRD".toLowerCase()) {
287237
const id = row.original.id;
288238
return <CRDSource source={source} id={id} showMinimal />;
289239
}
290240

241+
if (!createdBy) {
242+
return null;
243+
}
244+
291245
return <Avatar user={createdBy} />;
292246
}
293247
}

0 commit comments

Comments
 (0)