Skip to content

Commit 527e7b8

Browse files
committed
feat: add scope selector tab to permission form
- Add Scope tab to permission resource selector - Create multi-select dropdown for scopes with namespace display - Store selected scopes in object_selector.scopes as {namespace?, name} - Update TypeScript types to support scope selectors - Display scopes in permission resource view
1 parent 051406f commit 527e7b8

File tree

5 files changed

+125
-14
lines changed

5 files changed

+125
-14
lines changed

src/api/types/permissions.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,21 @@ export type PermissionGlobalObject =
1313
| "playbook"
1414
| "topology";
1515

16+
type PermissionObjectSelector = {
17+
playbooks?: Selectors[];
18+
connections?: Selectors[];
19+
configs?: Selectors[];
20+
components?: Selectors[];
21+
scopes?: ScopeSelector[];
22+
};
23+
24+
interface Selectors {}
25+
26+
interface ScopeSelector {
27+
namespace?: string;
28+
name: string;
29+
}
30+
1631
export type PermissionTable = {
1732
id: string;
1833
description: string;
@@ -35,7 +50,7 @@ export type PermissionTable = {
3550

3651
// Resources
3752
object?: PermissionGlobalObject;
38-
object_selector?: Record<string, any>[];
53+
object_selector?: PermissionObjectSelector;
3954
component_id?: string;
4055
canary_id?: string;
4156
config_id?: string;
@@ -81,12 +96,3 @@ export type PermissionsSummary = PermissionTable & {
8196
connection_object: Pick<Connection, "id" | "name" | "type">;
8297
component_object: Pick<Topology, "id" | "name" | "icon">;
8398
};
84-
85-
type PermissionObjectSelector = {
86-
playbooks: Selectors[];
87-
connections: Selectors[];
88-
configs: Selectors[];
89-
components: Selectors[];
90-
};
91-
92-
interface Selectors {}

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import FormikSelectDropdown from "@flanksource-ui/components/Forms/Formik/Formik
44
import { Switch } from "@flanksource-ui/ui/FormControls/Switch";
55
import { useFormikContext } from "formik";
66
import { useState } from "react";
7+
import FormikScopeMultiSelect from "./FormikScopeMultiSelect";
78

89
export const permissionObjectList = [
910
{ label: "Canaries", value: "canaries" },
@@ -30,18 +31,26 @@ export default function FormikPermissionSelectResourceFields() {
3031
| "Canary"
3132
| "Playbook"
3233
| "Connection"
33-
| "Global" => {
34+
| "Global"
35+
| "Scope" => {
3436
if (values.playbook_id) return "Playbook";
3537
if (values.config_id) return "Catalog";
3638
if (values.component_id) return "Component";
3739
if (values.connection_id) return "Connection";
3840
if (values.canary_id) return "Canary";
3941
if (values.object) return "Global";
42+
if (values.object_selector?.scopes) return "Scope";
4043
return "Catalog";
4144
};
4245

4346
const [switchOption, setSwitchOption] = useState<
44-
"Component" | "Catalog" | "Canary" | "Playbook" | "Connection" | "Global"
47+
| "Component"
48+
| "Catalog"
49+
| "Canary"
50+
| "Playbook"
51+
| "Connection"
52+
| "Global"
53+
| "Scope"
4554
>(getInitialTab());
4655

4756
return (
@@ -55,7 +64,8 @@ export default function FormikPermissionSelectResourceFields() {
5564
"Component",
5665
"Connection",
5766
"Playbook",
58-
"Global"
67+
"Global",
68+
"Scope"
5969
]}
6070
className="w-auto"
6171
itemsClassName=""
@@ -69,6 +79,8 @@ export default function FormikPermissionSelectResourceFields() {
6979
setFieldValue("canary_id", undefined);
7080
setFieldValue("component_id", undefined);
7181
setFieldValue("playbook_id", undefined);
82+
setFieldValue("connection_id", undefined);
83+
setFieldValue("object_selector", undefined);
7284
}}
7385
/>
7486
</div>
@@ -116,6 +128,8 @@ export default function FormikPermissionSelectResourceFields() {
116128
options={permissionObjectList}
117129
/>
118130
)}
131+
132+
{switchOption === "Scope" && <FormikScopeMultiSelect />}
119133
</div>
120134
</div>
121135
);
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { useScopesQuery } from "@flanksource-ui/api/query-hooks/useScopesQuery";
2+
import { useField, useFormikContext } from "formik";
3+
import { useMemo } from "react";
4+
import Select from "react-select";
5+
6+
type ScopeRef = {
7+
namespace?: string;
8+
name: string;
9+
};
10+
11+
type ScopeOption = {
12+
value: string;
13+
label: string;
14+
namespace?: string;
15+
name: string;
16+
};
17+
18+
export default function FormikScopeMultiSelect() {
19+
const { setFieldValue } = useFormikContext<Record<string, any>>();
20+
const [field] = useField<{ scopes?: ScopeRef[] }>({
21+
name: "object_selector"
22+
});
23+
24+
const { data: scopes, isLoading } = useScopesQuery();
25+
26+
const scopeOptions = useMemo(() => {
27+
if (!scopes) return [];
28+
return scopes.map((scope) => ({
29+
value: JSON.stringify({
30+
namespace: scope.namespace,
31+
name: scope.name
32+
}),
33+
label: scope.name,
34+
namespace: scope.namespace,
35+
name: scope.name
36+
}));
37+
}, [scopes]);
38+
39+
const selectedScopes = useMemo(() => {
40+
const scopeRefs = field.value?.scopes || [];
41+
return scopeRefs.map((scopeRef) => ({
42+
value: JSON.stringify(scopeRef),
43+
label: scopeRef.name,
44+
namespace: scopeRef.namespace,
45+
name: scopeRef.name
46+
}));
47+
}, [field.value]);
48+
49+
const formatOptionLabel = (option: ScopeOption) => (
50+
<div className="flex items-center justify-between gap-2">
51+
<span>{option.name}</span>
52+
{option.namespace && (
53+
<span className="text-xs text-gray-500">{option.namespace}</span>
54+
)}
55+
</div>
56+
);
57+
58+
return (
59+
<div className="mt-2 flex flex-col gap-2">
60+
<Select
61+
isMulti
62+
isLoading={isLoading}
63+
className="rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
64+
placeholder="Select..."
65+
options={scopeOptions}
66+
value={selectedScopes}
67+
formatOptionLabel={formatOptionLabel}
68+
onChange={(selectedOptions) => {
69+
const scopeRefs = selectedOptions.map((option) =>
70+
JSON.parse(option.value)
71+
);
72+
setFieldValue("object_selector", { scopes: scopeRefs });
73+
}}
74+
onBlur={field.onBlur}
75+
/>
76+
</div>
77+
);
78+
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ function PermissionActionDropdown({ isDisabled }: { isDisabled?: boolean }) {
4343
if (values.component_id) return "component";
4444
if (values.connection_id) return "connection";
4545
if (values.canary_id) return "canary";
46+
if (values.object_selector?.scopes) return "global";
4647
if (values.object) return "global";
4748
return undefined;
4849
}, [values]);
@@ -231,6 +232,7 @@ export default function PermissionForm({
231232
playbook_id: permissionData?.playbook_id,
232233
deny: permissionData?.deny ?? false,
233234
object: permissionData?.object,
235+
object_selector: permissionData?.object_selector,
234236
description: permissionData?.description,
235237
connection_id: permissionData?.connection_id,
236238
created_at: permissionData?.created_at,

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,30 @@ import { useFormikContext } from "formik";
66
import { permissionObjectList } from "./FormikPermissionSelectResourceFields";
77

88
export default function PermissionResource() {
9-
const { values } = useFormikContext<Record<string, string>>();
9+
const { values } = useFormikContext<Record<string, any>>();
1010

1111
const componentId = values.component_id;
1212
const playbookId = values.playbook_id;
1313
const configId = values.config_id;
1414
const connectionId = values.connection_id;
1515
const object = values.object;
16+
const objectSelector = values.object_selector;
1617

1718
if (object) {
1819
// eslint-disable-next-line react/jsx-no-useless-fragment
1920
return <>{permissionObjectList.find((o) => o.value === object)?.label}</>;
2021
}
2122

23+
if (objectSelector?.scopes) {
24+
const scopeNames = objectSelector.scopes
25+
.map((scope: { namespace?: string; name: string }) =>
26+
scope.namespace ? `${scope.namespace}/${scope.name}` : scope.name
27+
)
28+
.join(", ");
29+
// eslint-disable-next-line react/jsx-no-useless-fragment
30+
return <>Scopes: {scopeNames}</>;
31+
}
32+
2233
return (
2334
<div className="flex flex-row items-center">
2435
{componentId && (

0 commit comments

Comments
 (0)