Skip to content

Commit 460c8e1

Browse files
committed
feat: config access tab
1 parent 2934ba9 commit 460c8e1

File tree

7 files changed

+198
-0
lines changed

7 files changed

+198
-0
lines changed

src/App.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,12 @@ const ConfigDetailsRelationshipsPage = dynamic(() =>
227227
).then((mod) => mod.ConfigDetailsRelationshipsPage)
228228
);
229229

230+
const ConfigDetailsAccessPage = dynamic(() =>
231+
import("@flanksource-ui/pages/config/details/ConfigDetailsAccessPage").then(
232+
(mod) => mod.ConfigDetailsAccessPage
233+
)
234+
);
235+
230236
const ConfigDetailsViewPage = dynamic(() =>
231237
import("@flanksource-ui/pages/config/details/ConfigDetailsViewPage").then(
232238
(mod) => mod.ConfigDetailsViewPage
@@ -994,6 +1000,14 @@ export function IncidentManagerRoutes({ sidebar }: { sidebar: ReactNode }) {
9941000
"read"
9951001
)}
9961002
/>
1003+
<Route
1004+
path="access"
1005+
element={withAuthorizationAccessCheck(
1006+
<ConfigDetailsAccessPage />,
1007+
tables.database,
1008+
"read"
1009+
)}
1010+
/>
9971011
<Route
9981012
path="view/:viewId"
9991013
element={withAuthorizationAccessCheck(
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { useQuery } from "@tanstack/react-query";
2+
import { getConfigAccessSummary } from "../services/configs";
3+
4+
export default function useConfigAccessSummaryQuery(
5+
configId: string | undefined
6+
) {
7+
return useQuery({
8+
queryKey: ["config", "access-summary", configId],
9+
queryFn: () => getConfigAccessSummary(configId!),
10+
enabled: !!configId,
11+
keepPreviousData: true
12+
});
13+
}

src/api/services/configs.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { resolvePostGrestRequestWithPagination } from "../resolve";
88
import { PaginationInfo } from "../types/common";
99
import {
1010
ConfigAnalysis,
11+
ConfigAccessSummary,
1112
ConfigChange,
1213
ConfigHealthCheckView,
1314
ConfigItem,
@@ -162,6 +163,22 @@ export const getConfig = (id: string) =>
162163
ConfigDB.get(`/config_detail?id=eq.${id}&select=*`)
163164
);
164165

166+
export const getConfigAccessSummary = (configId: string) =>
167+
resolvePostGrestRequestWithPagination<ConfigAccessSummary[]>(
168+
ConfigDB.get(
169+
`/config_access_summary?config_id=eq.${encodeURIComponent(
170+
configId
171+
)}&select=user,email,role,external_group_id,last_signed_in_at,last_reviewed_at,created_at&order=${encodeURIComponent(
172+
"user.asc"
173+
)}`,
174+
{
175+
headers: {
176+
Prefer: "count=exact"
177+
}
178+
}
179+
)
180+
);
181+
165182
export type ConfigsTagList = {
166183
key: string;
167184
value: any;

src/api/types/configs.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,16 @@ export interface ConfigItemGraphData extends ConfigItem {
9797
isIntermediaryNode?: boolean;
9898
}
9999

100+
export interface ConfigAccessSummary {
101+
user: string;
102+
email: string;
103+
role?: string | null;
104+
external_group_id?: string | null;
105+
last_signed_in_at?: string | null;
106+
last_reviewed_at?: string | null;
107+
created_at: string;
108+
}
109+
100110
export interface ConfigTypeRelationships extends Timestamped {
101111
config_id: string;
102112
related_id: string;

src/components/Configs/ConfigDetailsTabs.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type ConfigDetailsTabsProps = {
2424
| "Relationships"
2525
| "Playbooks"
2626
| "Checks"
27+
| "Access"
2728
| string; // Views
2829
className?: string;
2930
extra?: ReactNode;

src/components/Configs/ConfigTabsLinks.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getViewsByConfigId } from "../../api/services/views";
55
import { useQuery } from "@tanstack/react-query";
66
import { Icon } from "@flanksource-ui/ui/Icons/Icon";
77
import { ReactNode } from "react";
8+
import useConfigAccessSummaryQuery from "@flanksource-ui/api/query-hooks/useConfigAccessSummaryQuery";
89

910
type ConfigDetailsTab = {
1011
label: ReactNode;
@@ -31,6 +32,10 @@ export function useConfigDetailsTabs(countSummary?: ConfigItem["summary"]): {
3132
enabled: !!id
3233
});
3334

35+
const { data: accessSummary } = useConfigAccessSummaryQuery(id);
36+
const accessCount =
37+
accessSummary?.totalEntries ?? accessSummary?.data?.length ?? 0;
38+
3439
const staticTabs: ConfigDetailsTab[] = [
3540
{ label: "Spec", key: "Spec", path: `/catalog/${id}/spec` },
3641
{
@@ -85,6 +90,19 @@ export function useConfigDetailsTabs(countSummary?: ConfigItem["summary"]): {
8590
}
8691
];
8792

93+
if (accessCount > 0) {
94+
staticTabs.push({
95+
label: (
96+
<>
97+
Access
98+
<Badge className="ml-1" text={accessCount} />
99+
</>
100+
),
101+
key: "Access",
102+
path: `/catalog/${id}/access`
103+
});
104+
}
105+
88106
const hasExplicitOrdering = views.some((view) => view.ordinal != null);
89107

90108
const orderedViews = hasExplicitOrdering
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import useConfigAccessSummaryQuery from "@flanksource-ui/api/query-hooks/useConfigAccessSummaryQuery";
2+
import { ConfigAccessSummary } from "@flanksource-ui/api/types/configs";
3+
import { ConfigDetailsTabs } from "@flanksource-ui/components/Configs/ConfigDetailsTabs";
4+
import { Age } from "@flanksource-ui/ui/Age";
5+
import { Badge } from "@flanksource-ui/ui/Badge/Badge";
6+
import { MRTCellProps } from "@flanksource-ui/ui/MRTDataTable/MRTCellProps";
7+
import MRTDataTable from "@flanksource-ui/ui/MRTDataTable/MRTDataTable";
8+
import { MRT_ColumnDef } from "mantine-react-table";
9+
import { useMemo } from "react";
10+
import { useParams } from "react-router-dom";
11+
12+
const RoleCell = ({ cell }: MRTCellProps<ConfigAccessSummary>) => {
13+
const value = cell.getValue<string | null>();
14+
return value ? (
15+
<span>{value}</span>
16+
) : (
17+
<span className="text-gray-400"></span>
18+
);
19+
};
20+
21+
const LastSignedInCell = ({ cell }: MRTCellProps<ConfigAccessSummary>) => {
22+
const value = cell.getValue<string | null>();
23+
if (!value) {
24+
return <span className="text-gray-400">Never</span>;
25+
}
26+
return <Age from={value} />;
27+
};
28+
29+
const OptionalDateCell = ({ cell }: MRTCellProps<ConfigAccessSummary>) => {
30+
const value = cell.getValue<string | null>();
31+
if (!value) {
32+
return <span className="text-gray-400"></span>;
33+
}
34+
return <Age from={value} />;
35+
};
36+
37+
const AccessTypeCell = ({ row }: MRTCellProps<ConfigAccessSummary>) => {
38+
const groupId = row.original.external_group_id;
39+
if (groupId) {
40+
return <Badge text="Group" color="yellow" title={`Group ID: ${groupId}`} />;
41+
}
42+
43+
return <Badge text="Direct" color="gray" />;
44+
};
45+
46+
export function ConfigDetailsAccessPage() {
47+
const { id } = useParams();
48+
const {
49+
data: accessSummary,
50+
isLoading,
51+
refetch
52+
} = useConfigAccessSummaryQuery(id);
53+
54+
const columns = useMemo<MRT_ColumnDef<ConfigAccessSummary>[]>(
55+
() => [
56+
{
57+
header: "User",
58+
accessorKey: "user",
59+
size: 160
60+
},
61+
{
62+
header: "Email",
63+
accessorKey: "email",
64+
size: 220
65+
},
66+
{
67+
header: "Role",
68+
accessorKey: "role",
69+
Cell: RoleCell,
70+
size: 120
71+
},
72+
{
73+
header: "Access",
74+
accessorKey: "external_group_id",
75+
Cell: AccessTypeCell,
76+
size: 120
77+
},
78+
{
79+
header: "Last Signed In",
80+
accessorKey: "last_signed_in_at",
81+
Cell: LastSignedInCell,
82+
sortingFn: "datetime",
83+
size: 160
84+
},
85+
{
86+
header: "Last Reviewed",
87+
accessorKey: "last_reviewed_at",
88+
Cell: OptionalDateCell,
89+
sortingFn: "datetime",
90+
size: 160
91+
},
92+
{
93+
header: "Granted",
94+
accessorKey: "created_at",
95+
Cell: OptionalDateCell,
96+
sortingFn: "datetime",
97+
size: 140
98+
}
99+
],
100+
[]
101+
);
102+
103+
const rows = accessSummary?.data ?? [];
104+
105+
return (
106+
<ConfigDetailsTabs
107+
pageTitlePrefix={"Catalog Access"}
108+
isLoading={isLoading}
109+
refetch={refetch}
110+
activeTabName="Access"
111+
>
112+
<div className="flex h-full flex-1 flex-col gap-2 overflow-y-auto">
113+
<div className="flex flex-1 flex-col overflow-y-auto">
114+
<MRTDataTable
115+
columns={columns}
116+
data={rows}
117+
isLoading={isLoading}
118+
disablePagination
119+
defaultSorting={[{ id: "user", desc: false }]}
120+
/>
121+
</div>
122+
</div>
123+
</ConfigDetailsTabs>
124+
);
125+
}

0 commit comments

Comments
 (0)