Skip to content

Commit 47f39ac

Browse files
committed
feat: add view for permission based on resource/team/person
Fixes #2246
1 parent f8de4a8 commit 47f39ac

File tree

6 files changed

+285
-2
lines changed

6 files changed

+285
-2
lines changed

src/api/services/permissions.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { AVATAR_INFO } from "@flanksource-ui/constants";
2+
import { IncidentCommander } from "../axios";
3+
import { resolvePostGrestRequestWithPagination } from "../resolve";
4+
import { PermissionAPIResponse } from "../types/permissions";
5+
6+
export type FetchPermissionsInput = {
7+
componentId?: string;
8+
personId?: string;
9+
teamId?: string;
10+
configId?: string;
11+
checkId?: string;
12+
canaryId?: string;
13+
playbookId?: string;
14+
};
15+
16+
function composeQueryParamForFetchPermissions({
17+
componentId,
18+
personId,
19+
teamId,
20+
configId,
21+
checkId,
22+
canaryId,
23+
playbookId
24+
}: FetchPermissionsInput) {
25+
if (componentId) {
26+
return `component_id=eq.${componentId}`;
27+
}
28+
if (personId) {
29+
return `person_id=eq.${personId}`;
30+
}
31+
if (teamId) {
32+
return `team_id=eq.${teamId}`;
33+
}
34+
if (configId) {
35+
return `config_id=eq.${configId}`;
36+
}
37+
if (checkId) {
38+
return `check_id=eq.${checkId}`;
39+
}
40+
if (canaryId) {
41+
return `canary_id=eq.${canaryId}`;
42+
}
43+
if (playbookId) {
44+
return `playbook_id=eq.${playbookId}`;
45+
}
46+
return undefined;
47+
}
48+
49+
export function fetchPermissions(
50+
input: FetchPermissionsInput,
51+
pagination: {
52+
pageSize: number;
53+
pageIndex: number;
54+
}
55+
) {
56+
const queryParam = composeQueryParamForFetchPermissions(input);
57+
const selectFields = [
58+
"*",
59+
"checks:check_id(id, name, status, type)",
60+
"catalog:config_id(id, name, type, config_class)",
61+
"component:component_id(id, name, icon)",
62+
"canary:canary_id(id, name, icon)",
63+
"playbook:playbook_id(id, title, name, icon)",
64+
"team:team_id(id, name, icon)",
65+
`person:person_id(${AVATAR_INFO})`,
66+
`createdBy:created_by(${AVATAR_INFO})`
67+
];
68+
69+
const { pageSize, pageIndex } = pagination;
70+
71+
const url = `/permissions?${queryParam}&select=${selectFields.join(",")}&limit=${pageSize}&offset=${pageIndex * pageSize}`;
72+
return resolvePostGrestRequestWithPagination(
73+
IncidentCommander.get<PermissionAPIResponse[]>(url)
74+
);
75+
}

src/api/types/permissions.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { ConfigItem } from "./configs";
2+
import { HealthCheck } from "./health";
3+
import { PlaybookSpec } from "./playbooks";
4+
import { Topology } from "./topology";
5+
import { Team, User } from "./users";
6+
7+
export type PermissionTable = {
8+
id: string;
9+
description: string;
10+
action: string;
11+
deny?: boolean;
12+
component_id?: string;
13+
config_id?: string;
14+
canary_id?: string;
15+
playbook_id?: string;
16+
created_by: string;
17+
person_id?: string;
18+
team_id?: string;
19+
updated_by: string;
20+
created_at: string;
21+
updated_at: string;
22+
until?: string;
23+
};
24+
25+
export type PermissionAPIResponse = PermissionTable & {
26+
checks: Pick<HealthCheck, "id" | "name" | "type" | "status">;
27+
catalog: Pick<ConfigItem, "id" | "name" | "type" | "config_class">;
28+
component: Pick<Topology, "id" | "name" | "icon">;
29+
canary: Pick<Topology, "id" | "name" | "icon">;
30+
playbook: Pick<PlaybookSpec, "id" | "name" | "icon" | "title">;
31+
team: Pick<Team, "id" | "name" | "icon">;
32+
person: User;
33+
createdBy: User;
34+
};

src/components/Canary/CanaryLink.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Icon } from "@flanksource-ui/ui/Icons/Icon";
2+
import { Link } from "react-router-dom";
3+
4+
type CanaryLinkProps = {
5+
canary: {
6+
id: string;
7+
name: string;
8+
icon?: string;
9+
};
10+
};
11+
12+
export default function CanaryLink({ canary }: CanaryLinkProps) {
13+
return (
14+
<Link
15+
className="flex flex-row items-center gap-1"
16+
to={{
17+
pathname: `/settings/canaries/${canary.id}`
18+
}}
19+
>
20+
{canary.icon && <Icon name={canary.icon} className="h-5" />}
21+
<span>{canary.name}</span>
22+
</Link>
23+
);
24+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { PermissionAPIResponse } from "@flanksource-ui/api/types/permissions";
2+
import { Avatar } from "@flanksource-ui/ui/Avatar";
3+
import { Badge } from "@flanksource-ui/ui/Badge/Badge";
4+
import { MRTDateCell } from "@flanksource-ui/ui/MRTDataTable/Cells/MRTDateCells";
5+
import MRTDataTable from "@flanksource-ui/ui/MRTDataTable/MRTDataTable";
6+
import { MRT_ColumnDef } from "mantine-react-table";
7+
import CanaryLink from "../Canary/CanaryLink";
8+
import { CheckLink } from "../Canary/HealthChecks/CheckLink";
9+
import ConfigLink from "../Configs/ConfigLink/ConfigLink";
10+
import PlaybookSpecIcon from "../Playbooks/Settings/PlaybookSpecIcon";
11+
import { TopologyLink } from "../Topology/TopologyLink";
12+
13+
const permissionsTableColumns: MRT_ColumnDef<PermissionAPIResponse>[] = [
14+
{
15+
id: "Resource",
16+
header: "Resource",
17+
Cell: ({ row }) => {
18+
const config = row.original.catalog;
19+
const check = row.original.checks;
20+
const playbook = row.original.playbook;
21+
const canary = row.original.canary;
22+
const component = row.original.component;
23+
24+
return (
25+
<div className="flex flex-col">
26+
{config && <ConfigLink config={config} />}
27+
{check && <CheckLink check={check} />}
28+
{playbook && <PlaybookSpecIcon playbook={playbook} showLabel />}
29+
{canary && <CanaryLink canary={canary} />}
30+
{component && <TopologyLink topology={component} />}
31+
</div>
32+
);
33+
}
34+
},
35+
{
36+
id: "action",
37+
header: "Action",
38+
Cell: ({ row }) => {
39+
const action = row.original.action;
40+
const deny = row.original.deny;
41+
42+
return (
43+
<div>
44+
<span>{action}</span>
45+
{deny && <Badge text="deny" />}
46+
</div>
47+
);
48+
}
49+
},
50+
{
51+
id: "updated",
52+
header: "Updated",
53+
accessorFn: (row) => row.updated_at,
54+
Cell: MRTDateCell
55+
},
56+
{
57+
id: "created",
58+
header: "Created",
59+
accessorFn: (row) => row.created_at
60+
},
61+
{
62+
id: "createdBy",
63+
header: "Created By",
64+
Cell: ({ row }) => {
65+
const createdBy = row.original.createdBy;
66+
return <Avatar user={createdBy} />;
67+
}
68+
}
69+
];
70+
71+
type PermissionsTableProps = {
72+
permissions: PermissionAPIResponse[];
73+
isLoading: boolean;
74+
pageCount: number;
75+
totalEntries: number;
76+
};
77+
78+
export default function PermissionsTable({
79+
permissions,
80+
isLoading,
81+
pageCount,
82+
totalEntries
83+
}: PermissionsTableProps) {
84+
return (
85+
<MRTDataTable<PermissionAPIResponse>
86+
columns={permissionsTableColumns}
87+
data={permissions}
88+
isLoading={isLoading}
89+
manualPageCount={pageCount}
90+
totalRowCount={totalEntries}
91+
enableServerSidePagination
92+
/>
93+
);
94+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
fetchPermissions,
3+
FetchPermissionsInput
4+
} from "@flanksource-ui/api/services/permissions";
5+
import useReactTablePaginationState from "@flanksource-ui/ui/DataTable/Hooks/useReactTablePaginationState";
6+
import { useQuery } from "@tanstack/react-query";
7+
import { useMemo } from "react";
8+
import PermissionsTable from "./PermissionsTable";
9+
10+
type PermissionsViewProps = {
11+
permissionRequest: FetchPermissionsInput;
12+
};
13+
14+
export default function PermissionsView({
15+
permissionRequest
16+
}: PermissionsViewProps) {
17+
const { pageSize, pageIndex } = useReactTablePaginationState();
18+
19+
const isEnabled = useMemo(() => {
20+
return Object.values(permissionRequest).some(
21+
(value) => value !== undefined
22+
);
23+
}, [permissionRequest]);
24+
25+
const { isLoading, data } = useQuery({
26+
queryKey: [
27+
"permissions",
28+
permissionRequest,
29+
{
30+
pageIndex,
31+
pageSize
32+
}
33+
],
34+
queryFn: () =>
35+
fetchPermissions(permissionRequest, {
36+
pageIndex,
37+
pageSize
38+
}),
39+
enabled: isEnabled
40+
});
41+
42+
const totalEntries = data?.totalEntries || 0;
43+
const pageCount = totalEntries ? Math.ceil(totalEntries / pageSize) : -1;
44+
const permissions = data?.data || [];
45+
46+
return (
47+
<PermissionsTable
48+
permissions={permissions}
49+
isLoading={isLoading}
50+
pageCount={pageCount}
51+
totalEntries={totalEntries}
52+
/>
53+
);
54+
}

src/ui/Badge/Badge.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ type BadgeProps = {
44
text: React.ReactNode;
55
value?: string;
66
size?: "xs" | "sm" | "md";
7-
color?: "blue" | "gray";
7+
color?: "blue" | "gray" | "yellow";
88
dot?: string;
99
title?: string;
1010
className?: string;
@@ -29,7 +29,9 @@ export function Badge({
2929
const colorClass =
3030
color === "blue"
3131
? "bg-blue-100 text-blue-800"
32-
: "bg-gray-100 text-gray-700";
32+
: color === "yellow"
33+
? "bg-yellow-100 text-yellow-800"
34+
: "bg-gray-100 text-gray-700";
3335
const spanClassName =
3436
size === "sm" ? "text-sm px-1 py-0.5" : "text-xs px-1 py-0.5";
3537
const svgClassName =

0 commit comments

Comments
 (0)