Skip to content

Commit ffa2e94

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

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: 15 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
{
@@ -82,6 +87,16 @@ export function useConfigDetailsTabs(countSummary?: ConfigItem["summary"]): {
8287
),
8388
key: "Checks",
8489
path: `/catalog/${id}/checks`
90+
},
91+
{
92+
label: (
93+
<>
94+
Access
95+
<Badge className="ml-1" text={accessCount} />
96+
</>
97+
),
98+
key: "Access",
99+
path: `/catalog/${id}/access`
85100
}
86101
];
87102

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
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 { DataTable } from "@flanksource-ui/ui/DataTable";
7+
import { CellContext, ColumnDef } from "@tanstack/table-core";
8+
import { SortingState } from "@tanstack/react-table";
9+
import { useMemo, useState } from "react";
10+
import { useParams } from "react-router-dom";
11+
12+
const RoleCell = ({ getValue }: CellContext<ConfigAccessSummary, any>) => {
13+
const value = getValue();
14+
return value ? (
15+
<span>{value}</span>
16+
) : (
17+
<span className="text-gray-400"></span>
18+
);
19+
};
20+
21+
const LastSignedInCell = ({
22+
getValue
23+
}: CellContext<ConfigAccessSummary, any>) => {
24+
const value = getValue();
25+
if (!value) {
26+
return <span className="text-gray-400">Never</span>;
27+
}
28+
return <Age from={value} />;
29+
};
30+
31+
const OptionalDateCell = ({
32+
getValue
33+
}: CellContext<ConfigAccessSummary, any>) => {
34+
const value = getValue();
35+
if (!value) {
36+
return <span className="text-gray-400"></span>;
37+
}
38+
return <Age from={value} />;
39+
};
40+
41+
const AccessTypeCell = ({ row }: CellContext<ConfigAccessSummary, any>) => {
42+
const groupId = row.original.external_group_id;
43+
if (groupId) {
44+
return <Badge text="Group" color="yellow" title={`Group ID: ${groupId}`} />;
45+
}
46+
47+
return <Badge text="Direct" color="gray" />;
48+
};
49+
50+
export function ConfigDetailsAccessPage() {
51+
const { id } = useParams();
52+
const {
53+
data: accessSummary,
54+
isLoading,
55+
refetch
56+
} = useConfigAccessSummaryQuery(id);
57+
const [sortBy, setSortBy] = useState<SortingState>([
58+
{ id: "user", desc: false }
59+
]);
60+
61+
const columns = useMemo<ColumnDef<ConfigAccessSummary>[]>(
62+
() => [
63+
{
64+
header: "User",
65+
accessorKey: "user"
66+
},
67+
{
68+
header: "Email",
69+
accessorKey: "email"
70+
},
71+
{
72+
header: "Role",
73+
accessorKey: "role",
74+
cell: RoleCell
75+
},
76+
{
77+
header: "Access",
78+
accessorKey: "external_group_id",
79+
cell: AccessTypeCell
80+
},
81+
{
82+
header: "Last Signed In",
83+
accessorKey: "last_signed_in_at",
84+
cell: LastSignedInCell,
85+
sortingFn: "datetime"
86+
},
87+
{
88+
header: "Last Reviewed",
89+
accessorKey: "last_reviewed_at",
90+
cell: OptionalDateCell,
91+
sortingFn: "datetime"
92+
},
93+
{
94+
header: "Granted",
95+
accessorKey: "created_at",
96+
cell: OptionalDateCell,
97+
sortingFn: "datetime"
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+
<DataTable
115+
stickyHead
116+
columns={columns}
117+
data={rows}
118+
tableStyle={{ borderSpacing: "0" }}
119+
isLoading={isLoading}
120+
className="table-auto"
121+
tableSortByState={sortBy}
122+
onTableSortByChanged={setSortBy}
123+
/>
124+
</div>
125+
</div>
126+
</ConfigDetailsTabs>
127+
);
128+
}

0 commit comments

Comments
 (0)