Skip to content

Commit

Permalink
Add Subordinate IDs main page
Browse files Browse the repository at this point in the history
The 'Subordinate IDs' page should show the
data related to a given user that has a
subordinate ID and its range.

The current solution provides functionality
for some buttons ('Refresh') and pagination.

The main page is being rendered using a
reusable component `MainPage` that allows
listing elements with some basic configuration
(showing links or checkboxes, and selection
functionality).

Signed-off-by: Carla Martinez <carlmart@redhat.com>
  • Loading branch information
carma12 committed Feb 27, 2025
1 parent 80c2e08 commit eb9f80f
Show file tree
Hide file tree
Showing 7 changed files with 901 additions and 5 deletions.
250 changes: 250 additions & 0 deletions src/components/tables/MainTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import React from "react";
// PatternFly
import { Td, Th, Tr } from "@patternfly/react-table";
// Tables
import TableLayout from "../layouts/TableLayout";
// Layouts
import SkeletonOnTableLayout from "../layouts/Skeleton/SkeletonOnTableLayout";
// Data types
import { SubId } from "src/utils/datatypes/globalDataTypes";
// React router DOM
import { Link } from "react-router-dom";
import EmptyBodyTable from "./EmptyBodyTable";

/**
* This component renders a table with the specified columns and rows.
* It also allows the user to select rows and perform operations on them.
*
*/

type DataType = SubId; // TODO: add more data types separated by an 'or' operator ('|')

interface SelectedElementsData {
isElementSelectable: (element: DataType) => boolean;
selectedElements: DataType[];
selectableElementsTable: DataType[];
setElementsSelected: (rule: DataType, isSelecting?: boolean) => void;
clearSelectedElements: () => void;
}

interface ButtonsData {
updateIsDeleteButtonDisabled: (value: boolean) => void;
isDeletion: boolean;
updateIsDeletion: (value: boolean) => void;
updateIsEnableButtonDisabled?: (value: boolean) => void;
updateIsDisableButtonDisabled?: (value: boolean) => void;
isDisableEnableOp?: boolean;
updateIsDisableEnableOp?: (value: boolean) => void;
}

interface PaginationData {
selectedPerPage: number;
updateSelectedPerPage: (selected: number) => void;
}

export interface PropsToTable {
tableTitle: string;
shownElementsList: DataType[];
pk: string; // E.g. Primary key for users --> "uid"
keyNames: string[]; // E.g. for user.uid, user.description --> ["uid", "description"]
columnNames: string[]; // E.g. ["User ID", "Description"]
hasCheckboxes: boolean;
pathname: string; // E.g. "active-users" (without the leading '/')
showTableRows: boolean;
showLink: boolean;
elementsData?: SelectedElementsData;
buttonsData?: ButtonsData;
paginationData?: PaginationData;
}

const MainTable = (props: PropsToTable) => {
// Retrieve elements data from props
const shownElementsList = [...props.shownElementsList];
const columnNames = [...props.columnNames];

// When user status is updated, unselect selected rows
React.useEffect(() => {
if (props.buttonsData && props.buttonsData.isDisableEnableOp) {
props.elementsData?.clearSelectedElements();
}
}, [props.buttonsData?.isDisableEnableOp]);

const isElementSelected = (element: DataType) => {
if (
props.elementsData?.selectedElements.find(
(selectedElement) => selectedElement[props.pk] === element[props.pk]
)
) {
return true;
} else {
return false;
}
};

// To allow shift+click to select/deselect multiple rows
const [recentSelectedRowIndex, setRecentSelectedRowIndex] = React.useState<
number | null
>(null);
const [shifting, setShifting] = React.useState(false);

// On selecting one single row
const onSelectElement = (
element: DataType,
rowIndex: number,
isSelecting: boolean
) => {
// If the element is shift + selecting the checkboxes, then all intermediate checkboxes should be selected
if (shifting && recentSelectedRowIndex !== null) {
const numberSelected = rowIndex - recentSelectedRowIndex;
const intermediateIndexes = Array.from(
new Array(Math.abs(numberSelected) + 1),
(_, i) => i + Math.min(recentSelectedRowIndex, rowIndex)
);
intermediateIndexes.forEach((index) =>
props.elementsData?.setElementsSelected(
shownElementsList[index],
isSelecting
)
);
} else {
props.elementsData?.setElementsSelected(element, isSelecting);
}
setRecentSelectedRowIndex(rowIndex);

// Resetting 'isDisableEnableOp'
props.buttonsData?.updateIsDeleteButtonDisabled(false);

// Update elementSelected array
if (isSelecting) {
// Increment the elements selected per page (++)
props.paginationData?.updateSelectedPerPage(
props.paginationData?.selectedPerPage + 1
);
} else {
// Decrement the elements selected per page (--)
props.paginationData?.updateSelectedPerPage(
props.paginationData?.selectedPerPage - 1
);
}
};

// Reset 'selectedElements array if a delete operation has been done
React.useEffect(() => {
if (props.buttonsData?.isDeletion) {
props.elementsData?.clearSelectedElements();
props.buttonsData.updateIsDeletion(false);
}
}, [props.buttonsData?.isDeletion]);

// Enable 'Delete' button (if any element selected)
React.useEffect(() => {
if (props.elementsData && props.elementsData.selectedElements.length > 0) {
props.buttonsData?.updateIsDeleteButtonDisabled(false);
}

if (
props.elementsData &&
props.elementsData.selectedElements.length === 0
) {
props.buttonsData?.updateIsDeleteButtonDisabled(true);
}
}, [props.elementsData?.selectedElements]);

// Keyboard event
React.useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Shift") {
setShifting(true);
}
};
const onKeyUp = (e: KeyboardEvent) => {
if (e.key === "Shift") {
setShifting(false);
}
};

document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp);

return () => {
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("keyup", onKeyUp);
};
}, []);

// Defining table header and body from here to avoid passing specific names to the Table Layout
const header = (
<Tr key="header" id="table-header">
{props.hasCheckboxes && <Th modifier="wrap"></Th>}
{props.columnNames.map((columnName, idx) => (
<Th modifier="wrap" key={idx}>
{columnName}
</Th>
))}
</Tr>
);

const body = shownElementsList.map((element, rowIndex) => {
if (element !== undefined) {
return (
<Tr key={"row-" + rowIndex} id={"row-" + rowIndex}>
{/* Checkboxes (if specified) */}
{props.hasCheckboxes && (
<Td
key={rowIndex}
id={rowIndex.toString()}
dataLabel="checkbox"
select={{
rowIndex,
onSelect: (_event, isSelecting) =>
onSelectElement(element, rowIndex, isSelecting),
isSelected: isElementSelected(element),
isDisabled: !props.elementsData?.isElementSelectable(element),
}}
/>
)}
{/* Table rows */}
{props.keyNames.map((keyName, idx) => (
<Td dataLabel={columnNames[keyName]} key={idx} id={idx.toString()}>
{idx === 0 && !!props.showLink ? (
<Link
to={"/" + props.pathname + "/" + element[keyName]}
state={element}
>
{element[keyName]}
</Link>
) : (
<>{element[keyName]}</>
)}
</Td>
))}
</Tr>
);
} else {
return <EmptyBodyTable key={"empty-row-" + rowIndex} />;
}
});

const skeleton = (
<SkeletonOnTableLayout
rows={4}
colSpan={9}
screenreaderText={"Loading table rows"}
/>
);

return (
<TableLayout
ariaLabel={props.tableTitle}
variant={"compact"}
hasBorders={true}
classes={"pf-v5-u-mt-md"}
tableId={props.pathname + "-table"}
isStickyHeader={true}
tableHeader={header}
tableBody={!props.showTableRows ? skeleton : body}
/>
);
};

export default MainTable;
4 changes: 4 additions & 0 deletions src/navigation/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import ResetPasswordPage from "src/login/ResetPasswordPage";
import SetupBrowserConfig from "src/pages/SetupBrowserConfig";
import Configuration from "src/pages/Configuration/Configuration";
import SyncOtpPage from "src/login/SyncOtpPage";
import SubordinateIDs from "src/pages/SubordinateIDs/SubordinateIDs";

// Renders routes (React)
export const AppRoutes = ({ isInitialDataLoaded }): React.ReactElement => {
Expand Down Expand Up @@ -336,6 +337,9 @@ export const AppRoutes = ({ isInitialDataLoaded }): React.ReactElement => {
/>
</Route>
</Route>
<Route path="subordinate-ids">
<Route path="" element={<SubordinateIDs />} />
</Route>
<Route path="hbac-services">
<Route path="" element={<HBACServices />} />
<Route path=":cn">
Expand Down
16 changes: 16 additions & 0 deletions src/navigation/NavRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ const IdViewsGroupRef = "id-views";
const AutomemberGroupRef = "automember";
const UserGroupRulesGroupRef = "user-group-rules";
const HostGroupRulesGroupRef = "host-group-rules";
// - Subordinate IDs
const SubordinateIDsGroupRef = "subordinate-ids";
// POLICY
// - Host-based access control
const HostBasedAccessControlGroupRef = "host-based-access-control";
Expand Down Expand Up @@ -173,6 +175,20 @@ export const navigationRoutes = [
},
],
},
{
label: "Subordinate IDs",
group: SubordinateIDsGroupRef,
title: `${BASE_TITLE} - Subordinate IDs`,
path: "",
items: [
{
label: "Subordinate IDs",
group: SubordinateIDsGroupRef,
title: `${BASE_TITLE} - Subordinate IDs`,
path: "subordinate-ids",
},
],
},
],
},
{
Expand Down
Loading

0 comments on commit eb9f80f

Please sign in to comment.