Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 71 additions & 7 deletions src/components/Permissions/PermissionsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Icon } from "@flanksource-ui/ui/Icons/Icon";
import { MRTDateCell } from "@flanksource-ui/ui/MRTDataTable/Cells/MRTDateCells";
import MRTDataTable from "@flanksource-ui/ui/MRTDataTable/MRTDataTable";
import { MRT_ColumnDef } from "mantine-react-table";
import { OnChangeFn, RowSelectionState } from "@tanstack/react-table";
import type { ChangeEvent } from "react";
import CanaryLink from "../Canary/CanaryLink";
import ConfigLink from "../Configs/ConfigLink/ConfigLink";
import ConnectionIcon from "../Connections/ConnectionIcon";
Expand Down Expand Up @@ -33,7 +35,6 @@ const permissionsTableColumns: MRT_ColumnDef<PermissionsSummary>[] = [
id: "subject",
accessorFn: (row) => row.subject,
header: "Subject",
size: 80,
Cell: ({ row }) => {
const { team, group, person, subject, notification, playbook } =
row.original;
Expand Down Expand Up @@ -112,7 +113,6 @@ const permissionsTableColumns: MRT_ColumnDef<PermissionsSummary>[] = [
header: "Resource",
enableHiding: true,
enableSorting: false,
size: 150,
Cell: ({ row }) => {
const config = row.original.config_object;
const playbook = row.original.playbook_object;
Expand Down Expand Up @@ -249,7 +249,6 @@ const permissionsTableColumns: MRT_ColumnDef<PermissionsSummary>[] = [
id: "action",
accessorFn: (row) => row.action,
header: "Action",
size: 60,
Cell: ({ row }) => {
const action = row.original.action;
const deny = row.original.deny;
Expand Down Expand Up @@ -280,19 +279,16 @@ const permissionsTableColumns: MRT_ColumnDef<PermissionsSummary>[] = [
id: "description",
header: "Description",
enableSorting: false,
size: 200,
accessorFn: (row) => row.description
},
{
id: "updated_at",
size: 40,
header: "Updated",
accessorFn: (row) => row.updated_at,
Cell: MRTDateCell
},
{
id: "created_at",
size: 40,
header: "Created",
accessorFn: (row) => row.created_at,
Cell: MRTDateCell
Expand All @@ -301,7 +297,6 @@ const permissionsTableColumns: MRT_ColumnDef<PermissionsSummary>[] = [
id: "created_by",
accessorFn: (row) => row.created_by,
header: "Created By",
size: 40,
Cell: ({ row }) => {
const createdBy = row.original.created_by;
const source = row.original.source;
Expand All @@ -325,6 +320,10 @@ type PermissionsTableProps = {
isLoading: boolean;
pageCount: number;
totalEntries: number;
enableRowSelection?: boolean;
rowSelection?: RowSelectionState;
onRowSelectionChange?: OnChangeFn<RowSelectionState>;
onSelectAllChange?: (checked: boolean) => void;
handleRowClick?: (row: PermissionsSummary) => void;
hideResourceColumn?: boolean;
};
Expand All @@ -334,19 +333,84 @@ export default function PermissionsTable({
isLoading,
pageCount,
totalEntries,
enableRowSelection = false,
rowSelection,
onRowSelectionChange,
onSelectAllChange,
hideResourceColumn = false,
handleRowClick = () => {}
}: PermissionsTableProps) {
return (
<MRTDataTable
key={
enableRowSelection
? "permissions-table-bulk"
: "permissions-table-default"
}
columns={permissionsTableColumns}
data={permissions}
defaultPageSize={5}
isLoading={isLoading}
manualPageCount={pageCount}
totalRowCount={totalEntries}
enableServerSidePagination
enableServerSideSorting
enableRowSelection={enableRowSelection}
enableSelectAll={enableRowSelection}
getRowId={(row) => row.id}
rowSelection={rowSelection}
onRowSelectionChange={onRowSelectionChange}
mantineSelectCheckboxProps={{
size: "xs",
radius: "lg",
styles: {
icon: {
display: "none"
},
input: {
opacity: 0.9,
borderWidth: 1,
borderColor: "#d1d5db",
backgroundColor: "#f9fafb",
transition:
"opacity 120ms ease-in-out, border-color 120ms ease-in-out",
"&:hover, &:focus": {
opacity: 1,
borderColor: "#9ca3af"
}
}
}
}}
mantineSelectAllCheckboxProps={{
size: "xs",
radius: "lg",
onChange: (event: ChangeEvent<HTMLInputElement>) => {
onSelectAllChange?.(event.currentTarget.checked);
},
styles: {
icon: {
display: "none"
},
input: {
opacity: 0.9,
borderWidth: 1,
borderColor: "#d1d5db",
backgroundColor: "#f9fafb",
transition:
"opacity 120ms ease-in-out, border-color 120ms ease-in-out",
"&:hover, &:focus": {
opacity: 1,
borderColor: "#9ca3af"
}
}
}
}}
onRowClick={handleRowClick}
displayColumnDefOptions={{
"mrt-row-select": {
maxSize: 36
}
}}
hiddenColumns={hideResourceColumn ? ["Resource"] : []}
/>
);
Expand Down
188 changes: 167 additions & 21 deletions src/components/Permissions/PermissionsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ import {
import useReactTablePaginationState from "@flanksource-ui/ui/DataTable/Hooks/useReactTablePaginationState";
import useReactTableSortState from "@flanksource-ui/ui/DataTable/Hooks/useReactTableSortState";
import { useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { Button } from "..";
import { toastSuccess } from "../Toast/toast";
import useMrtBulkSelection, {
BulkSelectionPayload
} from "@flanksource-ui/ui/MRTDataTable/Hooks/useMrtBulkSelection";
import { FormikSelectDropdownOption } from "../Forms/Formik/FormikSelectDropdown";
import PermissionForm from "./ManagePermissions/Forms/PermissionForm";
import PermissionsTable from "./PermissionsTable";
Expand Down Expand Up @@ -72,13 +76,31 @@ export function getActionsForResourceType(
return commonActions;
}

type SelectionScope = {
permissionRequest: FetchPermissionsInput;
sortBy?: string;
sortOrder: "asc" | "desc";
};

export type PermissionsBulkActionControls = {
hasSelectedRows: boolean;
selectionSummary: string;
onAllowSelected: () => void;
onDenySelected: () => void;
onClearSelection: () => void;
};

type PermissionsViewProps = {
permissionRequest: FetchPermissionsInput;
setIsLoading?: (isLoading: boolean) => void;
hideResourceColumn?: boolean;
newPermissionData?: Partial<PermissionTable>;
showAddPermission?: boolean;
showInlineBulkActionControls?: boolean;
onRefetch?: (refetch: () => void) => void;
onBulkActionControlsChange?: (
controls: PermissionsBulkActionControls
) => void;
};

export default function PermissionsView({
Expand All @@ -87,7 +109,9 @@ export default function PermissionsView({
hideResourceColumn = false,
newPermissionData,
showAddPermission = false,
onRefetch
showInlineBulkActionControls = true,
onRefetch,
onBulkActionControlsChange
}: PermissionsViewProps) {
const [selectedPermission, setSelectedPermission] =
useState<PermissionsSummary>();
Expand Down Expand Up @@ -137,29 +161,151 @@ export default function PermissionsView({

const totalEntries = data?.totalEntries || 0;
const pageCount = totalEntries ? Math.ceil(totalEntries / pageSize) : 1;
const permissions = data?.data || [];
const permissions = useMemo(() => data?.data ?? [], [data?.data]);

const selectionScope = useMemo<SelectionScope>(
() => ({
permissionRequest,
sortBy: mappedSortBy,
sortOrder: sortState[0]?.desc ? "desc" : "asc"
}),
[mappedSortBy, permissionRequest, sortState]
);

const selectionResetKey = useMemo(
() => JSON.stringify(permissionRequest),
[permissionRequest]
);

const {
rowSelection,
selectedCount,
hasSelectedRows,
selectionSummary,
onRowSelectionChange: handleRowSelectionChange,
onSelectAllChange: handleSelectAllChange,
clearSelection: handleClearSelection,
buildPayload
} = useMrtBulkSelection<PermissionsSummary, SelectionScope>({
rows: permissions,
totalRowCount: totalEntries,
getRowId: (row) => row.id,
selectionScope,
resetKey: selectionResetKey
});

const handleBulkAction = useCallback(
(action: "allow" | "deny") => {
const payload: BulkSelectionPayload<SelectionScope> = buildPayload();

toastSuccess(
`${action === "allow" ? "Allow Selected" : "Deny Selected"} prepared for ${selectedCount} permission${selectedCount === 1 ? "" : "s"}`
);

// Frontend-only wiring for now. API call will use this payload in follow-up work.
void payload;
},
[buildPayload, selectedCount]
);

const handleAllowSelected = useCallback(() => {
handleBulkAction("allow");
}, [handleBulkAction]);

const handleDenySelected = useCallback(() => {
handleBulkAction("deny");
}, [handleBulkAction]);

useEffect(() => {
if (!onBulkActionControlsChange) {
return;
}

onBulkActionControlsChange({
hasSelectedRows,
selectionSummary,
onAllowSelected: handleAllowSelected,
onDenySelected: handleDenySelected,
onClearSelection: handleClearSelection
});
}, [
hasSelectedRows,
handleAllowSelected,
handleClearSelection,
handleDenySelected,
onBulkActionControlsChange,
selectionSummary
]);

return (
<>
{showAddPermission && (
<div className="flex flex-row items-center justify-between p-2">
<Button
onClick={() => {
setIsPermissionModalOpen(true);
}}
>
Add Permission
</Button>
<div className="flex h-full min-h-0 flex-col">
{showAddPermission && (
<div className="mb-2 flex flex-row flex-wrap items-center gap-2">
<Button
onClick={() => {
setIsPermissionModalOpen(true);
}}
>
Add Permission
</Button>
</div>
)}

<div className="relative flex min-h-0 flex-1 flex-col">
{showInlineBulkActionControls && (
<div className="pointer-events-none absolute inset-x-0 bottom-3 z-50 flex justify-center px-4">
<div
className={`pointer-events-auto flex flex-row flex-wrap items-center gap-1.5 rounded-xl border border-gray-300 bg-gray-100 px-2.5 py-1.5 shadow-lg transition-all duration-500 ease-out ${
hasSelectedRows
? "translate-y-0 opacity-100"
: "translate-y-2 opacity-0"
}`}
>
<span className="pr-1 text-xs font-medium text-gray-700">
{selectionSummary}
</span>
<Button
size="none"
disabled={!hasSelectedRows}
className="rounded border border-gray-400 bg-white px-2 py-1 text-xs leading-none text-gray-800 hover:bg-gray-50"
onClick={handleAllowSelected}
>
Allow Selected
</Button>
<Button
size="none"
disabled={!hasSelectedRows}
className="rounded border border-gray-400 bg-gray-200 px-2 py-1 text-xs leading-none text-gray-800 hover:bg-gray-300"
onClick={handleDenySelected}
>
Deny Selected
</Button>
<Button
size="none"
className="rounded border border-gray-400 bg-white px-2 py-1 text-xs leading-none text-gray-800 hover:bg-gray-50"
onClick={handleClearSelection}
>
Clear
</Button>
</div>
</div>
)}

<PermissionsTable
permissions={permissions}
isLoading={isLoading}
pageCount={pageCount}
totalEntries={totalEntries}
enableRowSelection
rowSelection={rowSelection}
onRowSelectionChange={handleRowSelectionChange}
onSelectAllChange={handleSelectAllChange}
handleRowClick={(row) => setSelectedPermission(row)}
hideResourceColumn={hideResourceColumn}
/>
</div>
)}
<PermissionsTable
permissions={permissions}
isLoading={isLoading}
pageCount={pageCount}
totalEntries={totalEntries}
handleRowClick={(row) => setSelectedPermission(row)}
hideResourceColumn={hideResourceColumn}
/>
</div>
{selectedPermission && (
<PermissionForm
isOpen={!!selectedPermission}
Expand Down
Loading
Loading