diff --git a/package.json b/package.json
index cf77ef5398..817b1bc372 100644
--- a/package.json
+++ b/package.json
@@ -85,4 +85,4 @@
},
"version": "0.6.0",
"packageManager": "yarn@4.1.0"
-}
\ No newline at end of file
+}
diff --git a/packages/odyssey-react-mui/src/Card.tsx b/packages/odyssey-react-mui/src/Card.tsx
index 41c540ec05..7549944d7f 100644
--- a/packages/odyssey-react-mui/src/Card.tsx
+++ b/packages/odyssey-react-mui/src/Card.tsx
@@ -33,6 +33,7 @@ import {
useOdysseyDesignTokens,
} from "./OdysseyDesignTokensContext";
import { Heading5, Paragraph, Support } from "./Typography";
+import { Box } from "./Box";
export const CARD_IMAGE_HEIGHT = "64px";
@@ -91,30 +92,36 @@ const Card = ({
const cardContent = useMemo(
() => (
- <>
- {image && (
-
- {image}
-
- )}
-
- {overline && {overline}}
- {title && {title}}
- {description && (
- {description}
- )}
-
- {button && (
-
-
- {button}
-
-
- )}
- >
+
+
+ {image && (
+
+ {image}
+
+ )}
+
+ {overline && {overline}}
+ {title && {title}}
+ {description && (
+ {description}
+ )}
+
+ {button && (
+
+
+ {button}
+
+
+ )}
+
+
),
[
button,
diff --git a/packages/odyssey-react-mui/src/DataTable/DataTableRowActions.tsx b/packages/odyssey-react-mui/src/DataTable/DataTableRowActions.tsx
index 773aed4882..2e80e6407f 100644
--- a/packages/odyssey-react-mui/src/DataTable/DataTableRowActions.tsx
+++ b/packages/odyssey-react-mui/src/DataTable/DataTableRowActions.tsx
@@ -27,7 +27,7 @@ import { DataTableProps } from "./DataTable";
import { Trans, useTranslation } from "react-i18next";
export type DataTableRowActionsProps = {
- row: MRT_Row;
+ row: MRT_Row | MRT_RowData;
rowIndex: number;
rowActionButtons?: (
row: MRT_RowData,
diff --git a/packages/odyssey-react-mui/src/OdysseyCacheProvider.tsx b/packages/odyssey-react-mui/src/OdysseyCacheProvider.tsx
index 43742f4491..6132920bc5 100644
--- a/packages/odyssey-react-mui/src/OdysseyCacheProvider.tsx
+++ b/packages/odyssey-react-mui/src/OdysseyCacheProvider.tsx
@@ -51,7 +51,9 @@ const OdysseyCacheProvider = ({
key: uniqueAlphabeticalId,
nonce: nonce ?? window.cspNonce,
prepend: true,
- speedy: false, // <-- Needs to be set to false when shadow-dom is used!! https://github.com/emotion-js/emotion/issues/2053#issuecomment-713429122
+ // TODO: Change this back!!
+ speedy: true,
+ // speedy: false, // <-- Needs to be set to false when shadow-dom is used!! https://github.com/emotion-js/emotion/issues/2053#issuecomment-713429122
...(stylisPlugins && { stylisPlugins }),
});
}, [emotionRoot, nonce, stylisPlugins, uniqueAlphabeticalId]);
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/BulkActionsMenu.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/BulkActionsMenu.tsx
new file mode 100644
index 0000000000..cbe327bf4b
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/BulkActionsMenu.tsx
@@ -0,0 +1,85 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { memo, useCallback, Dispatch, SetStateAction } from "react";
+import { UniversalProps } from "./componentTypes";
+import { MenuButton } from "../../MenuButton";
+import { Button } from "../../Button";
+import { Box } from "../../Box";
+import { ChevronDownIcon } from "../../icons.generated";
+import { MRT_RowData, MRT_RowSelectionState } from "material-react-table";
+
+export type BulkActionMenuProps = {
+ data: MRT_RowData[];
+ menuItems: UniversalProps["bulkActionMenuItems"];
+ rowSelection: MRT_RowSelectionState;
+ setRowSelection: Dispatch>;
+};
+
+const BulkActionMenu = ({
+ data,
+ menuItems,
+ rowSelection,
+ setRowSelection,
+}: BulkActionMenuProps) => {
+ const selectedRowCount = Object.values(rowSelection).filter(
+ (value) => value === true,
+ ).length;
+
+ const handleSelectAll = useCallback(() => {
+ const rows = Object.fromEntries(data.map((row) => [row.id, true]));
+ setRowSelection(rows);
+ }, [data, setRowSelection]);
+
+ const handleSelectNone = useCallback(() => {
+ setRowSelection({});
+ }, [setRowSelection]);
+
+ return (
+
+ {selectedRowCount > 0 && (
+ }
+ buttonLabel={`${selectedRowCount} selected`}
+ ariaLabel="More actions"
+ >
+ {menuItems?.(rowSelection)}
+
+ )}
+
+
+
+
+
+ );
+};
+
+const MemoizedBulkActionMenu = memo(BulkActionMenu);
+MemoizedBulkActionMenu.displayName = "BulkActionMenu";
+
+export { MemoizedBulkActionMenu as BulkActionMenu };
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/DataStack.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/DataStack.tsx
new file mode 100644
index 0000000000..395e4efc28
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/DataStack.tsx
@@ -0,0 +1,93 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { memo } from "react";
+import { DataView } from "./DataView";
+import {
+ AvailableStackLayouts,
+ StackProps,
+ UniversalProps,
+} from "./componentTypes";
+import { availableStackLayouts } from "./constants";
+
+export type DataStackProps = UniversalProps &
+ StackProps & {
+ initialLayout?: (typeof availableStackLayouts)[number];
+ availableLayouts?: AvailableStackLayouts;
+ };
+
+const DataStack = ({
+ availableLayouts,
+ getData,
+ hasRowSelection,
+ onChangeRowSelection,
+ bulkActionMenuItems,
+ hasPagination,
+ currentPage,
+ paginationType,
+ resultsPerPage,
+ totalRows,
+ hasFilters,
+ hasSearch,
+ hasSearchSubmitButton,
+ hasRowReordering,
+ isRowReorderingDisabled,
+ filters,
+ searchDelayTime,
+ errorMessage,
+ emptyPlaceholder,
+ noResultsPlaceholder,
+ isLoading,
+ isEmpty,
+ isNoResults,
+ cardProps,
+ maxGridColumns,
+ rowActionMenuItems,
+}: DataStackProps) => {
+ return (
+
+ );
+};
+
+const MemoizedDataStack = memo(DataStack);
+MemoizedDataStack.displayName = "DataStack";
+
+export { MemoizedDataStack as DataStack };
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/DataTable.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/DataTable.tsx
new file mode 100644
index 0000000000..05c2b65015
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/DataTable.tsx
@@ -0,0 +1,95 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { memo } from "react";
+import { DataView } from "./DataView";
+import { TableProps, UniversalProps } from "./componentTypes";
+
+export type DataTableProps = UniversalProps & TableProps;
+
+const DataTable = ({
+ getData,
+ hasRowSelection,
+ onChangeRowSelection,
+ bulkActionMenuItems,
+ hasPagination,
+ currentPage,
+ paginationType,
+ resultsPerPage,
+ totalRows,
+ hasFilters,
+ hasSearch,
+ hasSearchSubmitButton,
+ hasRowReordering,
+ isRowReorderingDisabled,
+ filters,
+ searchDelayTime,
+ errorMessage,
+ emptyPlaceholder,
+ noResultsPlaceholder,
+ isLoading,
+ isEmpty,
+ isNoResults,
+ columns,
+ initialDensity,
+ hasChangeableDensity,
+ hasColumnResizing,
+ hasColumnVisibility,
+ renderDetailPanel,
+ rowActionButtons,
+ rowActionMenuItems,
+ hasSorting,
+}: DataTableProps) => {
+ return (
+
+ );
+};
+
+const MemoizedDataTable = memo(DataTable);
+MemoizedDataTable.displayName = "DataTable";
+
+export { MemoizedDataTable as DataTable };
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx
new file mode 100644
index 0000000000..2d365ae245
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/DataView.tsx
@@ -0,0 +1,388 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { memo, useEffect, useMemo, useState } from "react";
+import {
+ Layout,
+ UniversalProps,
+ ViewProps,
+ TableState,
+} from "./componentTypes";
+import {
+ availableLayouts as allAvailableLayouts,
+ densityValues,
+} from "./constants";
+import { Box } from "../../Box";
+import { DataFilter, DataFilters } from "../DataFilters";
+import { useFilterConversion } from "./useFilterConversion";
+import { BulkActionMenu } from "./BulkActionsMenu";
+import { LayoutSwitcher } from "./LayoutSwitcher";
+import { TableSettings } from "./TableSettings";
+import { Callout } from "../../Callout";
+import { Pagination, usePagination } from "../../Pagination";
+import { t } from "i18next";
+import { fetchData } from "./fetchData";
+import { TableContent } from "./TableContent";
+import { StackContent } from "./StackContent";
+import { useRowReordering } from "../../DataTable/useRowReordering";
+import { EmptyState } from "../../EmptyState";
+import {
+ MRT_Row,
+ MRT_RowData,
+ MRT_RowSelectionState,
+} from "material-react-table";
+
+export type DataViewProps = UniversalProps & ViewProps;
+
+const DataView = ({
+ getData: getDataFn,
+ getRowId: getRowIdProp,
+ availableLayouts = allAvailableLayouts,
+ initialLayout,
+ hasSearch,
+ hasFilters,
+ hasSearchSubmitButton,
+ searchDelayTime,
+ bulkActionMenuItems,
+ hasPagination,
+ paginationType = "paged",
+ resultsPerPage = 20,
+ currentPage = 1,
+ totalRows,
+ errorMessage: errorMessageProp,
+ hasRowReordering,
+ isRowReorderingDisabled,
+ onReorderRows,
+ emptyPlaceholder,
+ noResultsPlaceholder,
+ tableOptions,
+ stackOptions,
+ hasRowSelection,
+ onChangeRowSelection,
+ isEmpty: isEmptyProp,
+ isLoading: isLoadingProp,
+ isNoResults: isNoResultsProp,
+}: DataViewProps) => {
+ const [currentLayout, setCurrentLayout] = useState(
+ initialLayout ?? availableLayouts[0],
+ );
+
+ const [data, setData] = useState([]);
+ const [isLoading, setIsLoading] = useState(isLoadingProp ?? true);
+ const [isEmpty, setIsEmpty] = useState(isEmptyProp ?? true);
+ const [isNoResults, setIsNoResults] = useState(
+ isNoResultsProp ?? false,
+ );
+ const [errorMessage, setErrorMessage] =
+ useState(errorMessageProp);
+
+ const [search, setSearch] = useState("");
+ const [initialFilters, setInitialFilters] = useState();
+ const [filters, setFilters] = useState();
+
+ const [draggingRow, setDraggingRow] = useState | null>();
+
+ const [rowSelection, setRowSelection] = useState({});
+
+ useEffect(() => {
+ onChangeRowSelection?.(rowSelection);
+ }, [rowSelection, onChangeRowSelection]);
+
+ const [pagination, setPagination] = useState({
+ pageIndex: currentPage,
+ pageSize: resultsPerPage,
+ });
+
+ const [tableState, setTableState] = useState({
+ rowDensity: tableOptions?.initialDensity ?? densityValues[0],
+ columnSorting: [],
+ columnVisibility: {},
+ });
+
+ const hasMultipleAvailableLayouts = useMemo(
+ () => typeof availableLayouts !== "string" && availableLayouts.length > 1,
+ [availableLayouts],
+ );
+
+ const shouldShowFilters = useMemo(
+ () => hasSearch || hasFilters,
+ [hasSearch, hasFilters],
+ );
+
+ const availableFilters = useFilterConversion({
+ filters: filters,
+ columns: tableOptions?.columns,
+ });
+
+ const dataQueryParams = useMemo(
+ () => ({
+ page: pagination.pageIndex,
+ resultsPerPage: pagination.pageSize,
+ search,
+ filters,
+ sort: tableState?.columnSorting,
+ }),
+ [pagination, search, filters, tableState?.columnSorting],
+ );
+
+ const getRowId = getRowIdProp ? getRowIdProp : (row: MRT_RowData) => row.id;
+
+ // Set initial filters
+ useEffect(() => {
+ if (!initialFilters && filters) {
+ setInitialFilters(filters);
+ }
+ }, [filters, initialFilters]);
+
+ // Update pagination state if props change
+ useEffect(() => {
+ setPagination({
+ pageIndex: currentPage,
+ pageSize: resultsPerPage,
+ });
+ }, [currentPage, resultsPerPage]);
+
+ // Retrieve the data
+ useEffect(() => {
+ fetchData({
+ getDataFn,
+ setIsLoading,
+ setErrorMessage,
+ errorMessageProp,
+ setData,
+ dataQueryParams,
+ });
+ }, [dataQueryParams, errorMessageProp, getDataFn]);
+
+ // When data is updated
+ useEffect(() => {
+ setIsEmpty(
+ pagination.pageIndex === currentPage &&
+ pagination.pageSize === resultsPerPage &&
+ search === "" &&
+ filters === initialFilters &&
+ data.length === 0,
+ );
+ }, [
+ currentPage,
+ data,
+ filters,
+ initialFilters,
+ pagination,
+ resultsPerPage,
+ search,
+ ]);
+
+ // Change loading, empty and noResults state on prop change
+ useEffect(() => {
+ setIsLoading((prevValue) => isLoadingProp ?? prevValue);
+ }, [isLoadingProp]);
+
+ useEffect(() => {
+ setIsEmpty((prevValue) => isEmptyProp ?? prevValue);
+ }, [isEmptyProp]);
+
+ useEffect(() => {
+ setIsNoResults((prevValue) => isNoResultsProp ?? prevValue);
+ }, [isNoResultsProp]);
+
+ const emptyState = useMemo(() => {
+ const noResultsInnerContent = noResultsPlaceholder || (
+
+ );
+
+ if (isEmpty) {
+ return emptyPlaceholder || noResultsInnerContent;
+ }
+
+ if (isNoResults) {
+ return noResultsInnerContent;
+ }
+
+ return;
+ }, [emptyPlaceholder, noResultsPlaceholder, isEmpty, isNoResults]);
+
+ const additionalActions = useMemo(
+ () => (
+ <>
+ {currentLayout === "table" && tableOptions && (
+
+ )}
+
+ {hasMultipleAvailableLayouts && (
+
+ )}
+ >
+ ),
+ [
+ currentLayout,
+ tableOptions,
+ tableState,
+ hasMultipleAvailableLayouts,
+ availableLayouts,
+ ],
+ );
+
+ const { lastRow: lastRowOnPage } = usePagination({
+ pageIndex: pagination.pageIndex,
+ pageSize: pagination.pageSize,
+ totalRows,
+ });
+
+ const rowReorderingUtilities = useRowReordering({
+ totalRows,
+ onReorderRows,
+ data,
+ setData,
+ draggingRow,
+ setDraggingRow,
+ resultsPerPage: pagination.pageSize,
+ page: pagination.pageIndex,
+ });
+
+ return (
+
+ {errorMessage && (
+
+
+
+ )}
+
+ {shouldShowFilters && (
+
+ )}
+
+ {(bulkActionMenuItems || hasRowSelection) && (
+
+
+ {!shouldShowFilters && additionalActions}
+
+ )}
+
+ {!shouldShowFilters && !bulkActionMenuItems && !hasRowSelection && (
+
+ {additionalActions}
+
+ )}
+
+ {currentLayout === "table" && tableOptions && (
+
+ )}
+ {(currentLayout === "list" || currentLayout === "grid") &&
+ stackOptions && (
+
+ )}
+
+ {hasPagination && (
+
+ )}
+
+ );
+};
+
+const MemoizedDataView = memo(DataView);
+MemoizedDataView.displayName = "DataView";
+
+export { MemoizedDataView as DataView };
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/LayoutSwitcher.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/LayoutSwitcher.tsx
new file mode 100644
index 0000000000..83c0594a37
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/LayoutSwitcher.tsx
@@ -0,0 +1,65 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { Dispatch, memo, useCallback, SetStateAction } from "react";
+import { AvailableLayouts, Layout } from "./componentTypes";
+import { MenuButton } from "../../MenuButton";
+import { MenuItem } from "../../MenuItem";
+
+export type LayoutSwitcherProps = {
+ currentLayout: Layout;
+ availableLayouts: AvailableLayouts;
+ setCurrentLayout: Dispatch>;
+};
+
+const LayoutSwitcher = ({
+ currentLayout,
+ availableLayouts,
+ setCurrentLayout,
+}: LayoutSwitcherProps) => {
+ const changeLayout = useCallback(
+ (value: Layout) => (_event: React.MouseEvent) => {
+ // This is necessary to avoid linter errors, while the _event is necessary to satisfy the onClick type
+ if (process.env.NODE_ENV === "development") console.debug(_event);
+
+ setCurrentLayout(value);
+ },
+ [],
+ );
+
+ return (
+
+ <>
+ {typeof availableLayouts !== "string" &&
+ availableLayouts.map((value: Layout) => (
+
+ ))}
+ >
+
+ );
+};
+
+const MemoizedLayoutSwitcher = memo(LayoutSwitcher);
+MemoizedLayoutSwitcher.displayName = "LayoutSwitcher";
+
+export { MemoizedLayoutSwitcher as LayoutSwitcher };
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/RowActions.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/RowActions.tsx
new file mode 100644
index 0000000000..4ff23209e0
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/RowActions.tsx
@@ -0,0 +1,119 @@
+/*!
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { MRT_Row, MRT_RowData } from "material-react-table";
+import { Fragment, ReactElement, memo, useCallback } from "react";
+import { Button } from "../../Button";
+import { MenuItem } from "../../MenuItem";
+import { MenuButtonProps } from "../../MenuButton";
+import {
+ ArrowBottomIcon,
+ ArrowDownIcon,
+ ArrowTopIcon,
+ ArrowUpIcon,
+} from "../../icons.generated";
+import { DataTableProps } from "./DataTable";
+import { Trans } from "react-i18next";
+
+export type RowActionsProps = {
+ row: MRT_Row | MRT_RowData;
+ rowIndex: number;
+ rowActionButtons?: (
+ row: MRT_RowData,
+ ) => ReactElement;
+ rowActionMenuItems?: (row: MRT_RowData) => MenuButtonProps["children"];
+ isRowReorderingDisabled?: boolean;
+ totalRows?: DataTableProps["totalRows"];
+ updateRowOrder?: ({
+ rowId,
+ newRowIndex,
+ }: {
+ rowId: string;
+ newRowIndex: number;
+ }) => void;
+};
+
+const RowActions = ({
+ row,
+ rowIndex,
+ rowActionMenuItems,
+ isRowReorderingDisabled,
+ totalRows,
+ updateRowOrder,
+}: RowActionsProps) => {
+ const handleToFrontClick = useCallback(() => {
+ updateRowOrder && updateRowOrder({ rowId: row.id, newRowIndex: 0 });
+ }, [row.id, updateRowOrder]);
+
+ const handleForwardClick = useCallback(() => {
+ updateRowOrder &&
+ updateRowOrder({ rowId: row.id, newRowIndex: Math.max(0, rowIndex - 1) });
+ }, [row.id, rowIndex, updateRowOrder]);
+
+ const handleBackwardClick = useCallback(() => {
+ updateRowOrder &&
+ updateRowOrder({ rowId: row.id, newRowIndex: rowIndex + 1 });
+ }, [row.id, rowIndex, updateRowOrder]);
+
+ const handleToBackClick = useCallback(() => {
+ updateRowOrder &&
+ updateRowOrder({
+ rowId: row.id,
+ newRowIndex: totalRows ? totalRows - 1 : rowIndex,
+ });
+ }, [row.id, rowIndex, totalRows, updateRowOrder]);
+
+ return (
+ <>
+ {rowActionMenuItems && <>{rowActionMenuItems(row)}>}
+ {rowActionMenuItems && updateRowOrder &&
}
+ {updateRowOrder && (
+ <>
+
+
+
+ {totalRows && (
+
+ )}
+ >
+ )}
+ >
+ );
+};
+
+const MemoizedRowActions = memo(RowActions);
+MemoizedRowActions.displayName = "RowActions";
+
+export { MemoizedRowActions as RowActions };
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/StackCard.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/StackCard.tsx
new file mode 100644
index 0000000000..919a2f44de
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/StackCard.tsx
@@ -0,0 +1,194 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import {
+ MouseEventHandler,
+ ReactElement,
+ memo,
+ useMemo,
+ ReactNode,
+} from "react";
+import {
+ Card as MuiCard,
+ CardActions as MuiCardActions,
+ CardActionArea as MuiCardActionArea,
+} from "@mui/material";
+import styled from "@emotion/styled";
+
+import { Button } from "../../Button";
+import { ButtonContext } from "../../ButtonContext";
+import { MoreIcon } from "../../icons.generated";
+import { MenuButton, MenuButtonProps } from "../../MenuButton";
+import {
+ DesignTokens,
+ useOdysseyDesignTokens,
+} from "../../OdysseyDesignTokensContext";
+import { Heading5, Paragraph, Support } from "../../Typography";
+import { Box } from "../../Box";
+
+export const CARD_IMAGE_HEIGHT = "64px";
+
+export type StackCardProps = {
+ description?: string;
+ image?: ReactElement;
+ overline?: string;
+ title?: string;
+ children?: ReactNode;
+} & (
+ | {
+ onClick: MouseEventHandler;
+ button?: never;
+ menuButtonChildren?: never;
+ Accessory?: never;
+ }
+ | {
+ onClick?: never;
+ button?: ReactElement;
+ menuButtonChildren?: MenuButtonProps["children"];
+ Accessory?: ReactNode;
+ }
+);
+
+const ImageContainer = styled("div", {
+ shouldForwardProp: (prop) =>
+ prop !== "odysseyDesignTokens" && prop !== "hasMenuButtonChildren",
+})<{
+ odysseyDesignTokens: DesignTokens;
+ hasMenuButtonChildren: boolean;
+}>(({ odysseyDesignTokens, hasMenuButtonChildren }) => ({
+ display: "flex",
+ alignItems: "flex-start",
+ maxHeight: `${CARD_IMAGE_HEIGHT}`,
+ marginBlockEnd: odysseyDesignTokens.Spacing5,
+ paddingRight: hasMenuButtonChildren ? odysseyDesignTokens.Spacing5 : 0,
+}));
+
+const MenuButtonContainer = styled("div", {
+ shouldForwardProp: (prop) => prop !== "odysseyDesignTokens",
+})<{ odysseyDesignTokens: DesignTokens }>(({ odysseyDesignTokens }) => ({
+ position: "absolute",
+ right: odysseyDesignTokens.Spacing3,
+ top: odysseyDesignTokens.Spacing3,
+}));
+
+const buttonProviderValue = { isFullWidth: true };
+
+const StackCard = ({
+ button,
+ description,
+ image,
+ menuButtonChildren,
+ onClick,
+ overline,
+ title,
+ children,
+ Accessory,
+}: StackCardProps) => {
+ const odysseyDesignTokens = useOdysseyDesignTokens();
+
+ const cardContent = useMemo(
+ () => (
+
+ {Accessory && (
+
+ {Accessory}
+
+ )}
+
+ {image && (
+
+ {image}
+
+ )}
+
+ {overline && {overline}}
+ {title && {title}}
+ {description && (
+ {description}
+ )}
+
+ {button && (
+
+
+ {button}
+
+
+ )}
+
+ {children && (
+
+ {children}
+
+ )}
+
+
+ ),
+ [
+ button,
+ description,
+ image,
+ menuButtonChildren,
+ overline,
+ title,
+ odysseyDesignTokens,
+ children,
+ ],
+ );
+
+ return (
+
+ {onClick ? (
+ {cardContent}
+ ) : (
+ cardContent
+ )}
+
+ {menuButtonChildren && (
+
+ }
+ ariaLabel="Card menu"
+ buttonVariant="floating"
+ menuAlignment="right"
+ size="small"
+ tooltipText="Actions"
+ >
+ {menuButtonChildren}
+
+
+ )}
+
+ );
+};
+
+const MemoizedStackCard = memo(StackCard);
+MemoizedStackCard.displayName = "StackCard";
+
+export { MemoizedStackCard as StackCard };
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/StackContent.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/StackContent.tsx
new file mode 100644
index 0000000000..21c2c41160
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/StackContent.tsx
@@ -0,0 +1,238 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { Dispatch, ReactNode, SetStateAction, memo, useCallback } from "react";
+import { StackLayout, StackProps, UniversalProps } from "./componentTypes";
+import { Box } from "../../Box";
+import styled, { CSSObject } from "@emotion/styled";
+import {
+ MRT_Row,
+ MRT_RowData,
+ MRT_RowSelectionState,
+ MRT_TableInstance,
+} from "material-react-table";
+import { CircularProgress } from "../../CircularProgress";
+import { Checkbox as MuiCheckbox } from "@mui/material";
+import { RowActions } from "./RowActions";
+import { StackCard } from "./StackCard";
+import {
+ DesignTokens,
+ useOdysseyDesignTokens,
+} from "../../OdysseyDesignTokensContext";
+
+export type StackContentProps = {
+ currentLayout: StackLayout;
+ data: MRT_RowData[];
+ getRowId: UniversalProps["getRowId"];
+ stackOptions: StackProps;
+ isLoading: boolean;
+ isEmpty?: boolean;
+ isNoResults?: boolean;
+ hasRowReordering: UniversalProps["hasRowReordering"];
+ isRowReorderingDisabled?: boolean;
+ onReorderRows: UniversalProps["onReorderRows"];
+ totalRows: UniversalProps["totalRows"];
+ rowReorderingUtilities: {
+ dragHandleStyles: CSSObject;
+ dragHandleText: {
+ title: string;
+ "aria-label": string;
+ };
+ draggableTableBodyRowClassName: ({
+ currentRowId,
+ draggingRowId,
+ hoveredRowId,
+ }: {
+ currentRowId: string;
+ draggingRowId?: string;
+ hoveredRowId?: string;
+ }) => string | undefined;
+ handleDragHandleKeyDown: ({
+ table,
+ row,
+ event,
+ }: {
+ table: MRT_TableInstance;
+ row: MRT_Row;
+ event: React.KeyboardEvent;
+ }) => void;
+ handleDragHandleOnDragCapture: (
+ table: MRT_TableInstance,
+ ) => void;
+ handleDragHandleOnDragEnd: (table: MRT_TableInstance) => void;
+ resetDraggingAndHoveredRow: (table: MRT_TableInstance) => void;
+ updateRowOrder: ({
+ rowId,
+ newRowIndex,
+ }: {
+ rowId: string;
+ newRowIndex: number;
+ }) => void;
+ };
+ hasRowSelection: UniversalProps["hasRowSelection"];
+ rowSelection: MRT_RowSelectionState;
+ setRowSelection: Dispatch>;
+ emptyState: ReactNode;
+ pagination: {
+ pageIndex: number;
+ pageSize: number;
+ };
+ draggingRow?: MRT_Row | null;
+};
+
+const StackContent = ({
+ currentLayout,
+ data,
+ stackOptions,
+ isLoading,
+ isEmpty,
+ isNoResults,
+ hasRowReordering,
+ isRowReorderingDisabled,
+ onReorderRows,
+ rowReorderingUtilities,
+ hasRowSelection,
+ rowSelection,
+ setRowSelection,
+ emptyState,
+ pagination,
+ totalRows,
+}: StackContentProps) => {
+ const handleRowSelectionChange = useCallback(
+ (row: MRT_RowData) => {
+ setRowSelection((prev) => {
+ const newSelection = { ...prev };
+ if (newSelection[row.id]) {
+ delete newSelection[row.id];
+ } else {
+ newSelection[row.id] = true;
+ }
+ return newSelection;
+ });
+ },
+ [setRowSelection],
+ );
+
+ const { updateRowOrder } = rowReorderingUtilities;
+
+ const odysseyDesignTokens = useOdysseyDesignTokens();
+
+ const StackContainer = styled("div", {
+ shouldForwardProp: (prop) =>
+ prop !== "odysseyDesignTokens" &&
+ prop !== "currentLayout" &&
+ prop !== "maxGridColumns",
+ })<{
+ odysseyDesignTokens: DesignTokens;
+ currentLayout: StackLayout;
+ maxGridColumns: number;
+ }>(({ odysseyDesignTokens, currentLayout, maxGridColumns }) => ({
+ display: currentLayout === "stack" ? "flex" : "grid",
+ flexDirection: currentLayout === "stack" ? "column" : undefined,
+ gap: odysseyDesignTokens.Spacing5,
+ [`@media (max-width: 720px)`]: {
+ gridTemplateColumns:
+ currentLayout === "grid" ? "repeat(1, 1fr)" : undefined,
+ },
+ [`@media (min-width: 720px) and (max-width: 960px)`]: {
+ gridTemplateColumns:
+ currentLayout === "grid" ? "repeat(2, 1fr)" : undefined,
+ },
+ [`@media (min-width: 960px)`]: {
+ gridTemplateColumns:
+ currentLayout === "grid" ? `repeat(${maxGridColumns}, 1fr)` : undefined,
+ },
+ }));
+
+ const LoadingContainer = styled("div", {
+ shouldForwardProp: (prop) => prop !== "odysseyDesignTokens",
+ })<{
+ odysseyDesignTokens: DesignTokens;
+ }>(({ odysseyDesignTokens }) => ({
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ width: "100%",
+ paddingBlock: odysseyDesignTokens.Spacing5,
+ }));
+
+ return (
+
+ {isLoading ? (
+
+
+
+ ) : (
+ <>
+ {!data || data.length === 0 || isEmpty || isNoResults ? (
+ {emptyState}
+ ) : (
+ <>
+ {data.map((row: MRT_RowData, index: number) => {
+ const { overline, title, description, image, children } =
+ stackOptions.cardProps(row);
+ const currentIndex =
+ index + (pagination.pageIndex - 1) * pagination.pageSize;
+
+ return (
+
+ handleRowSelectionChange(row)}
+ />
+
+ )
+ }
+ key={row.id}
+ menuButtonChildren={
+ (stackOptions.rowActionMenuItems || hasRowReordering) && (
+
+ )
+ }
+ />
+ );
+ })}
+ >
+ )}
+ >
+ )}
+
+ );
+};
+
+const MemoizedStackContent = memo(StackContent);
+MemoizedStackContent.displayName = "StackContent";
+
+export { MemoizedStackContent as StackContent };
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/TableContent.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/TableContent.tsx
new file mode 100644
index 0000000000..a52cd77c89
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/TableContent.tsx
@@ -0,0 +1,361 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import {
+ MRT_Row,
+ MRT_RowData,
+ MRT_RowSelectionState,
+ MRT_TableContainer,
+ MRT_TableInstance,
+ MRT_TableOptions,
+ useMaterialReactTable,
+} from "material-react-table";
+import {
+ SetStateAction,
+ memo,
+ useCallback,
+ useMemo,
+ useRef,
+ useState,
+ ReactNode,
+ Dispatch,
+} from "react";
+import { TableProps, TableState, UniversalProps } from "./componentTypes";
+import { DataTableCell } from "./dataTypes";
+import {
+ ArrowDownIcon,
+ ArrowUnsortedIcon,
+ ChevronDownIcon,
+ DragIndicatorIcon,
+} from "../../icons.generated";
+import {
+ dataTableImmutableSettings,
+ displayColumnDefOptions,
+ ScrollableTableContainer,
+} from "./tableConstants";
+import { useScrollIndication } from "../../DataTable/useScrollIndication";
+import { useOdysseyDesignTokens } from "../../OdysseyDesignTokensContext";
+import { Box } from "../../Box";
+import { CSSObject } from "@emotion/styled";
+import { RowActions } from "./RowActions";
+import { MenuButton } from "../../MenuButton";
+import { MoreIcon } from "../../icons.generated";
+import { useTranslation } from "react-i18next";
+
+export type TableContentProps = {
+ data: MRT_RowData[];
+ columns: TableProps["columns"];
+ getRowId: UniversalProps["getRowId"];
+ tableState: TableState;
+ setTableState: Dispatch>;
+ tableOptions: TableProps;
+ isLoading: boolean;
+ isEmpty?: boolean;
+ isNoResults?: boolean;
+ hasRowReordering: UniversalProps["hasRowReordering"];
+ isRowReorderingDisabled?: boolean;
+ onReorderRows: UniversalProps["onReorderRows"];
+ totalRows: UniversalProps["totalRows"];
+ rowReorderingUtilities: {
+ dragHandleStyles: CSSObject;
+ dragHandleText: {
+ title: string;
+ "aria-label": string;
+ };
+ draggableTableBodyRowClassName: ({
+ currentRowId,
+ draggingRowId,
+ hoveredRowId,
+ }: {
+ currentRowId: string;
+ draggingRowId?: string;
+ hoveredRowId?: string;
+ }) => string | undefined;
+ handleDragHandleKeyDown: ({
+ table,
+ row,
+ event,
+ }: {
+ table: MRT_TableInstance;
+ row: MRT_Row;
+ event: React.KeyboardEvent;
+ }) => void;
+ handleDragHandleOnDragCapture: (
+ table: MRT_TableInstance,
+ ) => void;
+ handleDragHandleOnDragEnd: (table: MRT_TableInstance) => void;
+ resetDraggingAndHoveredRow: (table: MRT_TableInstance) => void;
+ updateRowOrder: ({
+ rowId,
+ newRowIndex,
+ }: {
+ rowId: string;
+ newRowIndex: number;
+ }) => void;
+ };
+ hasRowSelection: UniversalProps["hasRowSelection"];
+ rowSelection: MRT_RowSelectionState;
+ setRowSelection: Dispatch>;
+ emptyState: ReactNode;
+ pagination: {
+ pageIndex: number;
+ pageSize: number;
+ };
+ draggingRow?: MRT_Row | null;
+};
+
+const TableContent = ({
+ data,
+ columns,
+ getRowId,
+ tableOptions,
+ tableState,
+ setTableState,
+ isLoading,
+ isEmpty,
+ isNoResults,
+ hasRowReordering,
+ isRowReorderingDisabled,
+ onReorderRows,
+ rowReorderingUtilities,
+ hasRowSelection,
+ rowSelection,
+ setRowSelection,
+ emptyState,
+ pagination,
+ totalRows,
+ draggingRow,
+}: TableContentProps) => {
+ const [isTableContainerScrolledToStart, setIsTableContainerScrolledToStart] =
+ useState(true);
+ const [isTableContainerScrolledToEnd, setIsTableContainerScrolledToEnd] =
+ useState(true);
+ const [tableInnerContainerWidth, setTableInnerContainerWidth] =
+ useState("100%");
+ const tableOuterContainerRef = useRef(null);
+ const tableInnerContainerRef = useRef(null);
+ const tableContentRef = useRef(null);
+
+ useScrollIndication({
+ tableOuterContainer: tableOuterContainerRef.current,
+ tableInnerContainer: tableInnerContainerRef.current,
+ setIsTableContainerScrolledToStart: setIsTableContainerScrolledToStart,
+ setIsTableContainerScrolledToEnd: setIsTableContainerScrolledToEnd,
+ setTableInnerContainerWidth: setTableInnerContainerWidth,
+ });
+
+ const odysseyDesignTokens = useOdysseyDesignTokens();
+ const { t } = useTranslation();
+
+ const columnIds = useMemo(() => {
+ return columns.map((column) => column.accessorKey) ?? [];
+ }, [columns]);
+
+ const columnOrder = useMemo(
+ () => [
+ "mrt-row-drag",
+ "mrt-row-select",
+ "mrt-row-expand",
+ ...columnIds,
+ "mrt-row-actions",
+ ],
+ [columnIds],
+ ) as string[];
+
+ const rowDensityClassName = useMemo(() => {
+ return tableState.rowDensity === "spacious"
+ ? "MuiTableBody-spacious"
+ : tableState.rowDensity === "compact"
+ ? "MuiTableBody-compact"
+ : "MuiTableBody-default";
+ }, [tableState]);
+
+ const defaultCell = useCallback(
+ ({ cell }: { cell: DataTableCell }) => {
+ const value = cell.getValue();
+ const hasTextWrapping = cell.column.columnDef.hasTextWrapping;
+
+ return hasTextWrapping ? (
+ value
+ ) : (
+
+ {value}
+
+ );
+ },
+ [],
+ );
+
+ const {
+ dragHandleStyles,
+ dragHandleText,
+ draggableTableBodyRowClassName,
+ handleDragHandleKeyDown,
+ handleDragHandleOnDragCapture,
+ handleDragHandleOnDragEnd,
+ resetDraggingAndHoveredRow,
+ updateRowOrder,
+ } = rowReorderingUtilities;
+
+ const renderRowActions = useCallback(
+ ({ row }: { row: MRT_Row }) => {
+ const currentIndex =
+ row.index + (pagination.pageIndex - 1) * pagination.pageSize;
+ return (
+
+ {tableOptions.rowActionButtons?.(row)}
+ {(tableOptions.rowActionMenuItems || hasRowReordering) && (
+ }
+ size="small"
+ buttonVariant="floating"
+ ariaLabel={t("table.moreactions.arialabel")}
+ menuAlignment="right"
+ >
+
+
+ )}
+
+ );
+ },
+ [
+ pagination.pageIndex,
+ pagination.pageSize,
+ tableOptions,
+ hasRowReordering,
+ t,
+ isRowReorderingDisabled,
+ totalRows,
+ onReorderRows,
+ updateRowOrder,
+ ],
+ );
+
+ const emptyStateContainer = useCallback(
+ () => {emptyState},
+ [tableInnerContainerWidth, emptyState],
+ );
+
+ const dataTable = useMaterialReactTable({
+ data: !isEmpty && !isNoResults ? data : [],
+ columns,
+ getRowId,
+ state: {
+ sorting: tableState.columnSorting,
+ columnVisibility: tableState.columnVisibility,
+ isLoading: isLoading,
+ rowSelection: rowSelection,
+ columnOrder: columnOrder,
+ },
+ icons: {
+ ArrowDownwardIcon: ArrowDownIcon,
+ DragHandleIcon: DragIndicatorIcon,
+ SyncAltIcon: ArrowUnsortedIcon,
+ ExpandMoreIcon: ChevronDownIcon,
+ },
+ ...dataTableImmutableSettings,
+ displayColumnDefOptions:
+ displayColumnDefOptions as MRT_TableOptions["displayColumnDefOptions"],
+ muiTableProps: {
+ ref: tableContentRef,
+ },
+ muiTableContainerProps: {
+ ref: tableInnerContainerRef,
+ },
+ muiTableBodyProps: () => ({
+ className: rowDensityClassName,
+ }),
+ enableColumnResizing: tableOptions.hasColumnResizing,
+ defaultColumn: {
+ Cell: defaultCell,
+ },
+ enableRowActions:
+ (hasRowReordering === true && onReorderRows) ||
+ tableOptions.rowActionButtons ||
+ tableOptions.rowActionMenuItems
+ ? true
+ : false,
+ renderRowActions: ({ row }) => renderRowActions({ row }),
+ enableRowOrdering: hasRowReordering && Boolean(onReorderRows),
+ enableRowDragging: hasRowReordering && Boolean(onReorderRows),
+ muiTableBodyRowProps: ({ table, row }) => ({
+ className: draggableTableBodyRowClassName({
+ currentRowId: row.id,
+ draggingRowId: draggingRow?.id,
+ hoveredRowId: table.getState().hoveredRow?.id,
+ }),
+ }),
+ muiRowDragHandleProps: ({ table, row }) => ({
+ onKeyDown: (event) => handleDragHandleKeyDown({ table, row, event }),
+ onBlur: () => resetDraggingAndHoveredRow(table),
+ onDragEnd: () => handleDragHandleOnDragEnd(table),
+ onDragCapture: () => handleDragHandleOnDragCapture(table),
+ disabled: isRowReorderingDisabled,
+ sx: dragHandleStyles,
+ ...dragHandleText,
+ }),
+ renderDetailPanel: tableOptions.renderDetailPanel,
+ enableRowVirtualization: data.length >= 50,
+ muiTableHeadCellProps: ({ column: currentColumn }) => ({
+ className: tableState.columnSorting.find(
+ (sortedColumn) => sortedColumn.id === currentColumn.id,
+ )
+ ? "isSorted"
+ : "isUnsorted",
+ }),
+ enableSorting: tableOptions.hasSorting === true, // I don't know why this needs to be true, but it still works if undefined otherwise
+ onSortingChange: (sortingUpdater) => {
+ const newSortVal =
+ typeof sortingUpdater === "function"
+ ? sortingUpdater(tableState.columnSorting)
+ : tableState.columnSorting;
+ setTableState((prevState) => ({
+ ...prevState,
+ columnSorting: newSortVal,
+ }));
+ },
+ enableRowSelection: hasRowSelection,
+ onRowSelectionChange: setRowSelection,
+ renderEmptyRowsFallback: emptyStateContainer,
+ });
+
+ return (
+
+
+
+ );
+};
+
+const MemoizedTableContent = memo(TableContent);
+MemoizedTableContent.displayName = "TableContent";
+
+export { MemoizedTableContent as TableContent };
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/TableSettings.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/TableSettings.tsx
new file mode 100644
index 0000000000..83dc58ce37
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/TableSettings.tsx
@@ -0,0 +1,137 @@
+/*!
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { Dispatch, SetStateAction, memo, useCallback, useMemo } from "react";
+import { Checkbox as MuiCheckbox } from "@mui/material";
+import { MenuButton } from "../../MenuButton";
+import { MenuItem } from "../../MenuItem";
+import { ListIcon, ShowIcon } from "../../icons.generated";
+import { densityValues } from "./constants";
+import { useTranslation } from "react-i18next";
+import { TableProps, TableState } from "./componentTypes";
+
+export type TableSettingsProps = {
+ tableOptions: TableProps;
+ tableState: TableState;
+ setTableState: Dispatch>;
+};
+
+const TableSettings = ({
+ tableOptions,
+ tableState,
+ setTableState,
+}: TableSettingsProps) => {
+ const { t } = useTranslation();
+
+ const { hasChangeableDensity, hasColumnVisibility, columns } = tableOptions;
+ const { rowDensity, columnVisibility } = tableState;
+
+ const changeRowDensity = useCallback(
+ (value: (typeof densityValues)[number]) =>
+ (_event: React.MouseEvent) => {
+ // This is necessary to avoid linter errors, while the _event is necessary to satisfy the onClick type
+ if (process.env.NODE_ENV === "development") console.debug(_event);
+
+ setTableState((prevState) => ({
+ ...prevState,
+ rowDensity: value,
+ }));
+ },
+ [],
+ );
+
+ const changeColumnVisibility = useCallback(
+ (columnId: string) => (_event: React.MouseEvent) => {
+ // This is necessary to avoid linter errors, while the _event is necessary to satisfy the onClick type
+ if (process.env.NODE_ENV === "development") console.debug(_event);
+
+ setTableState((prevState) => ({
+ ...prevState,
+ columnVisibility: {
+ ...prevState.columnVisibility,
+ [columnId]: prevState.columnVisibility
+ ? prevState.columnVisibility[columnId] === false
+ : false,
+ },
+ }));
+ },
+ [],
+ );
+
+ const isColumnVisibilityChecked = useMemo(() => {
+ return columns.reduce(
+ (acc, column) => {
+ const isChecked = columnVisibility
+ ? columnVisibility[column.accessorKey!] !== false
+ : true;
+ acc[column.accessorKey!] = isChecked;
+ return acc;
+ },
+ {} as Record,
+ );
+ }, [columns, columnVisibility]);
+
+ return (
+ <>
+ {hasChangeableDensity && (
+ }
+ ariaLabel={t("table.density.arialabel")}
+ menuAlignment="right"
+ shouldCloseOnSelect={false}
+ >
+ <>
+ {densityValues.map((value: (typeof densityValues)[number]) => (
+
+ ))}
+ >
+
+ )}
+
+ {hasColumnVisibility && (
+ }
+ ariaLabel={t("table.columnvisibility.arialabel")}
+ menuAlignment="right"
+ shouldCloseOnSelect={false}
+ >
+ <>
+ {columns
+ .filter((column) => column.enableHiding !== false)
+ .map((column) => (
+
+ ))}
+ >
+
+ )}
+ >
+ );
+};
+
+const MemoizedTableSettings = memo(TableSettings);
+MemoizedTableSettings.displayName = "TableSettings";
+
+export { MemoizedTableSettings as TableSettings };
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts b/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts
new file mode 100644
index 0000000000..4c7c2fb832
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/componentTypes.ts
@@ -0,0 +1,118 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import {
+ MRT_DensityState,
+ MRT_RowData,
+ MRT_RowSelectionState,
+ MRT_SortingState,
+ MRT_TableOptions,
+ MRT_VisibilityState,
+} from "material-react-table";
+import { DataFilter } from "../DataFilters";
+import { paginationTypeValues } from "../DataTablePagination";
+import {
+ availableLayouts,
+ availableStackLayouts,
+ densityValues,
+} from "./constants";
+import { MenuButtonProps } from "../..";
+import { ReactNode } from "react";
+import { DataTableRowActionsProps } from "../../DataTable/DataTableRowActions";
+import {
+ DataGetDataType,
+ DataOnReorderRowsType,
+ DataRowSelectionState,
+ DataTableColumn,
+} from "./dataTypes";
+import { StackCardProps } from "./StackCard";
+
+export type Layout = (typeof availableLayouts)[number];
+export type StackLayout = (typeof availableStackLayouts)[number];
+
+export type AvailableLayouts = Layout[];
+export type AvailableStackLayouts = StackLayout[];
+
+export type UniversalProps = {
+ // Data handling
+ getData: ({
+ page,
+ resultsPerPage,
+ search,
+ filters,
+ sort,
+ }: DataGetDataType) => MRT_RowData[] | Promise;
+ getRowId?: MRT_TableOptions["getRowId"];
+ hasRowReordering?: boolean;
+ isRowReorderingDisabled?: boolean;
+ onReorderRows?: ({ rowId, newRowIndex }: DataOnReorderRowsType) => void;
+
+ // Row selection
+ hasRowSelection?: boolean;
+ onChangeRowSelection?: (rowSelection: DataRowSelectionState) => void;
+ bulkActionMenuItems?: (
+ selectedRows: MRT_RowSelectionState,
+ ) => MenuButtonProps["children"];
+
+ // Pagination
+ hasPagination?: boolean;
+ currentPage?: number;
+ paginationType?: (typeof paginationTypeValues)[number];
+ resultsPerPage?: number;
+ totalRows?: number;
+
+ // Search & filtering
+ hasFilters?: boolean;
+ hasSearch?: boolean;
+ hasSearchSubmitButton?: boolean;
+ filters?: Array | string>;
+ searchDelayTime?: number;
+
+ // States
+ errorMessage?: string;
+ emptyPlaceholder?: ReactNode;
+ noResultsPlaceholder?: ReactNode;
+ isLoading?: boolean;
+ isEmpty?: boolean;
+ isNoResults?: boolean;
+};
+
+export type TableProps = {
+ columns: DataTableColumn[];
+ initialDensity?: (typeof densityValues)[number];
+ hasChangeableDensity?: boolean;
+ hasColumnResizing?: boolean;
+ hasColumnVisibility?: boolean;
+ renderDetailPanel?: MRT_TableOptions["renderDetailPanel"];
+ rowActionButtons?: DataTableRowActionsProps["rowActionButtons"];
+ rowActionMenuItems?: DataTableRowActionsProps["rowActionMenuItems"];
+ hasSorting?: boolean;
+};
+
+export type StackProps = {
+ cardProps: (row: MRT_RowData) => StackCardProps;
+ maxGridColumns?: number;
+ rowActionMenuItems?: DataTableRowActionsProps["rowActionMenuItems"];
+};
+
+export type ViewProps = {
+ initialLayout?: L;
+ availableLayouts?: L[];
+ tableOptions?: TableProps;
+ stackOptions?: StackProps;
+};
+
+export type TableState = {
+ columnSorting: MRT_SortingState;
+ columnVisibility: MRT_VisibilityState;
+ rowDensity?: MRT_DensityState;
+};
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/constants.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/constants.tsx
new file mode 100644
index 0000000000..8838fe1b58
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/constants.tsx
@@ -0,0 +1,20 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+export const availableStackLayouts = ["stack", "grid"];
+export const availableTableLayouts = ["table"];
+export const availableLayouts = [
+ ...availableTableLayouts,
+ ...availableStackLayouts,
+];
+
+export const densityValues = ["comfortable", "spacious", "compact"] as const;
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/dataTypes.ts b/packages/odyssey-react-mui/src/labs/DataComponents/dataTypes.ts
new file mode 100644
index 0000000000..a04149e5af
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/dataTypes.ts
@@ -0,0 +1,72 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import {
+ MRT_Cell,
+ MRT_Column,
+ MRT_ColumnDef,
+ MRT_RowData,
+ MRT_RowSelectionState,
+ MRT_SortingState,
+} from "material-react-table";
+import { DataFilter } from "../DataFilters";
+
+export type DataQueryParamsType = {
+ page?: number;
+ resultsPerPage?: number;
+ search?: string;
+ filters?: DataFilter[];
+ sort?: MRT_SortingState;
+};
+
+export type DataTableColumn = MRT_ColumnDef & {
+ hasTextWrapping?: boolean;
+};
+
+export type DataTableColumnInstance = Omit<
+ MRT_Column,
+ "columnDef"
+> & {
+ columnDef: DataTableColumn;
+};
+
+export type DataTableCell = Omit<
+ MRT_Cell,
+ "column"
+> & {
+ column: DataTableColumnInstance;
+};
+
+export type DataColumns = DataTableColumn[];
+
+export type DataRow = MRT_RowData;
+
+export type DataGetDataType = {
+ page?: number;
+ resultsPerPage?: number;
+ search?: string;
+ filters?: DataFilter[];
+ sort?: MRT_SortingState;
+};
+
+export type DataOnReorderRowsType = {
+ rowId: string;
+ newRowIndex: number;
+};
+
+export type DataRowSelectionState = MRT_RowSelectionState;
+
+// Provided for backwards compatibilty with old DataTable types
+export type DataTableGetDataType = DataGetDataType;
+export type DataTableOnReorderRowsType = DataOnReorderRowsType;
+export type DataTableRowSelectionState = DataRowSelectionState;
+export type DataTableRow = DataRow;
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/fetchData.ts b/packages/odyssey-react-mui/src/labs/DataComponents/fetchData.ts
new file mode 100644
index 0000000000..3f6e5463c9
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/fetchData.ts
@@ -0,0 +1,49 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { Dispatch, SetStateAction } from "react";
+import { UniversalProps } from "./componentTypes";
+import { DataQueryParamsType } from "./dataTypes";
+import { t } from "i18next";
+import { MRT_RowData } from "material-react-table";
+
+type DataRequestType = {
+ getDataFn: UniversalProps["getData"];
+ setIsLoading: Dispatch>;
+ setErrorMessage: Dispatch>;
+ errorMessageProp: UniversalProps["errorMessage"];
+ setData: Dispatch>;
+ dataQueryParams: DataQueryParamsType;
+};
+
+export const fetchData = async ({
+ getDataFn,
+ setIsLoading,
+ setErrorMessage,
+ errorMessageProp,
+ setData,
+ dataQueryParams,
+}: DataRequestType) => {
+ setIsLoading(true);
+ setErrorMessage(errorMessageProp);
+ try {
+ const incomingData = await getDataFn?.(dataQueryParams);
+ // incomingData.forEach((item: MRT_RowData, index: number) => {
+ // item._ods_index = index;
+ // });
+ setData(incomingData);
+ } catch (error) {
+ setErrorMessage(typeof error === "string" ? error : t("table.error"));
+ } finally {
+ setIsLoading(false);
+ }
+};
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/index.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/index.tsx
new file mode 100644
index 0000000000..4742bfae82
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/index.tsx
@@ -0,0 +1,19 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+export { DataView, type DataViewProps } from "./DataView";
+export { DataTable, type DataTableProps } from "./DataTable";
+export { DataStack, type DataStackProps } from "./DataStack";
+
+export * from "./constants";
+export * from "./componentTypes";
+export * from "./dataTypes";
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/tableConstants.tsx b/packages/odyssey-react-mui/src/labs/DataComponents/tableConstants.tsx
new file mode 100644
index 0000000000..caf704862c
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/tableConstants.tsx
@@ -0,0 +1,156 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { MRT_RowData, MRT_TableOptions } from "material-react-table";
+import { DragIndicatorIcon } from "../../icons.generated";
+import { Box } from "../../Box";
+import { DesignTokens } from "../../OdysseyDesignTokensContext";
+import styled from "@emotion/styled";
+
+export const dataTableImmutableSettings = {
+ enableColumnActions: false,
+ enableDensityToggle: false,
+ enableFilters: false,
+ enableFullScreenToggle: false,
+ enableGlobalFilter: false,
+ enableHiding: false,
+ enablePagination: false,
+ layoutMode: "grid-no-grow" as MRT_TableOptions["layoutMode"],
+ manualFiltering: true,
+ manualSorting: true,
+ muiTablePaperProps: {
+ elevation: 0,
+ sx: {
+ overflow: "visible",
+ },
+ },
+ selectAllMode: "all" as MRT_TableOptions["selectAllMode"],
+ positionActionsColumn:
+ "last" as MRT_TableOptions["positionActionsColumn"],
+ rowVirtualizerOptions: {
+ overscan: 4,
+ },
+ enableExpandAll: false,
+};
+
+export const displayColumnDefOptions = {
+ "mrt-row-actions": {
+ header: "",
+ grow: true,
+ muiTableBodyCellProps: {
+ align: "right",
+ sx: {
+ overflow: "visible",
+ width: "unset",
+ },
+ className: "ods-actions-cell",
+ },
+ muiTableHeadCellProps: {
+ align: "right",
+ sx: {
+ width: "unset",
+ },
+ className: "ods-actions-cell",
+ },
+ },
+ "mrt-row-drag": {
+ header: "",
+ muiTableBodyCellProps: {
+ sx: {
+ minWidth: 0,
+ width: "auto",
+ },
+ className: "ods-drag-handle",
+ },
+ muiTableHeadCellProps: {
+ sx: {
+ minWidth: 0,
+ width: "auto",
+ },
+ children: (
+ // Add a spacer to simulate the width of the drag handle in the column.
+ // Without this, the head cells are offset from their body cell counterparts
+
+
+
+ ),
+ },
+ },
+ "mrt-row-select": {
+ muiTableHeadCellProps: {
+ padding: "checkbox",
+ },
+ muiTableBodyCellProps: {
+ padding: "checkbox",
+ },
+ },
+ "mrt-row-expand": {
+ header: "",
+ },
+};
+
+export const ScrollableTableContainer = styled("div", {
+ shouldForwardProp: (prop) =>
+ prop !== "odysseyDesignTokens" &&
+ prop !== "isScrollableStart" &&
+ prop !== "isScrollableEnd",
+})(
+ ({
+ odysseyDesignTokens,
+ isScrollableStart,
+ isScrollableEnd,
+ }: {
+ odysseyDesignTokens: DesignTokens;
+ isScrollableStart: boolean;
+ isScrollableEnd: boolean;
+ }) => ({
+ marginBlockEnd: odysseyDesignTokens.Spacing4,
+ position: "relative",
+ borderInlineStartColor: isScrollableStart
+ ? odysseyDesignTokens.HueNeutral200
+ : "transparent",
+ borderInlineStartStyle: "solid",
+ borderInlineStartWidth: odysseyDesignTokens.BorderWidthMain,
+ "::before": {
+ background:
+ "linear-gradient(-90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.33) 50%, rgba(0, 0, 0, 1) 100%)",
+ content: '""',
+ opacity: isScrollableStart ? "0.075" : "0",
+ pointerEvents: "none",
+ position: "absolute",
+ top: 0,
+ left: 0,
+ bottom: 0,
+ width: odysseyDesignTokens.Spacing6,
+ zIndex: 100,
+ transition: `opacity ${odysseyDesignTokens.TransitionDurationMain} ${odysseyDesignTokens.TransitionTimingMain}`,
+ },
+ borderInlineEndColor: isScrollableEnd
+ ? odysseyDesignTokens.HueNeutral200
+ : "transparent",
+ borderInlineEndStyle: "solid",
+ borderInlineEndWidth: odysseyDesignTokens.BorderWidthMain,
+ "::after": {
+ background:
+ "linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.33) 50%, rgba(0, 0, 0, 1) 100%)",
+ content: '""',
+ opacity: isScrollableEnd ? "0.075" : "0",
+ pointerEvents: "none",
+ position: "absolute",
+ top: 0,
+ right: 0,
+ bottom: 0,
+ width: odysseyDesignTokens.Spacing6,
+ transition: `opacity ${odysseyDesignTokens.TransitionDurationMain} ${odysseyDesignTokens.TransitionTimingMain}`,
+ },
+ }),
+);
diff --git a/packages/odyssey-react-mui/src/labs/DataComponents/useFilterConversion.ts b/packages/odyssey-react-mui/src/labs/DataComponents/useFilterConversion.ts
new file mode 100644
index 0000000000..0ef9cc40f4
--- /dev/null
+++ b/packages/odyssey-react-mui/src/labs/DataComponents/useFilterConversion.ts
@@ -0,0 +1,91 @@
+/*!
+ * Copyright (c) 2024-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { useCallback, useMemo } from "react";
+import { DataFilter } from "../DataFilters";
+import { UniversalProps, TableProps } from "./componentTypes";
+import { DataTableColumn } from "../../DataTable";
+import { MRT_RowData } from "material-react-table";
+
+type FilterConversionType = {
+ filters?: UniversalProps["filters"];
+ columns?: TableProps["columns"];
+};
+
+export const useFilterConversion = ({
+ filters,
+ columns,
+}: FilterConversionType) => {
+ const convertFilterSelectOptions = useCallback(
+ (options: DataTableColumn["filterSelectOptions"]) =>
+ options?.map((option) =>
+ typeof option === "string"
+ ? {
+ label: option,
+ value: option,
+ }
+ : {
+ // If the option isn't a string, it must have value and/or option defined
+ // If either is undefined, use the other
+ label: option.label ?? option.value,
+ value: option.value ?? option.label,
+ },
+ ),
+ [],
+ );
+
+ const convertColumnToFilter = useCallback(
+ (column: DataTableColumn) =>
+ column.enableColumnFilter !== false && column.accessorKey
+ ? ({
+ id: column.accessorKey,
+ label: column.header,
+ variant: column.filterVariant,
+ options: convertFilterSelectOptions(column.filterSelectOptions),
+ } satisfies DataFilter as DataFilter)
+ : null,
+ [convertFilterSelectOptions],
+ );
+
+ const dataTableFilters = useMemo(() => {
+ const providedFilters = filters || columns;
+ if (!providedFilters) {
+ return [];
+ }
+ return providedFilters.reduce((accumulator, item) => {
+ if (typeof item === "string") {
+ const foundColumn = columns?.find(
+ (column) => column.accessorKey === item,
+ );
+ if (foundColumn) {
+ const filter = convertColumnToFilter(foundColumn);
+ if (filter) {
+ return accumulator.concat(filter);
+ }
+ }
+ } else if ("accessorKey" in item) {
+ // Checks if it's a column
+ const filter = convertColumnToFilter(item);
+ if (filter) {
+ return accumulator.concat(filter);
+ }
+ } else if ("label" in item) {
+ // Checks if it's a DataFilter
+ return accumulator.concat(item);
+ }
+ // If none of the conditions match, item is ignored (not mapping to undefined)
+ return accumulator;
+ }, []);
+ }, [columns, filters]);
+
+ return dataTableFilters;
+};
diff --git a/packages/odyssey-react-mui/src/labs/index.ts b/packages/odyssey-react-mui/src/labs/index.ts
index e6710d69b8..0e10af7f0f 100644
--- a/packages/odyssey-react-mui/src/labs/index.ts
+++ b/packages/odyssey-react-mui/src/labs/index.ts
@@ -17,6 +17,8 @@ export type { LocalizationProviderProps } from "@mui/x-date-pickers";
export * from "./DatePicker";
export * from "./datePickerTheme";
+export * from "./DataComponents";
+
/** @deprecated Will be removed in a future Odyssey version in lieu of the one shipping with DataTable */
export * from "./DataTablePagination";
export * from "./DataFilters";
diff --git a/packages/odyssey-react-mui/src/theme/components.tsx b/packages/odyssey-react-mui/src/theme/components.tsx
index 94cd03edd9..c9e1b7edfb 100644
--- a/packages/odyssey-react-mui/src/theme/components.tsx
+++ b/packages/odyssey-react-mui/src/theme/components.tsx
@@ -2630,14 +2630,15 @@ export const components = ({
[`.${tableHeadClasses.root} &`]: {
color: odysseyTokens.TypographyColorHeading,
- fontSize: `0.71428571rem`,
- lineHeight: odysseyTokens.TypographyLineHeightBody,
fontWeight: odysseyTokens.TypographyWeightBodyBold,
textTransform: "uppercase",
backgroundColor: odysseyTokens.HueNeutral50,
borderBottom: 0,
height: `${odysseyTokens.Spacing7} !important`,
paddingBlock: `${odysseyTokens.Spacing3} !important`,
+ fontSize: odysseyTokens.TypographySizeOverline,
+ lineHeight: odysseyTokens.TypographyLineHeightBody,
+ letterSpacing: 1.3,
},
[`.${tableHeadClasses.root} &:first-of-type`]: {
diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/DataComponents/DataComponents.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-labs/DataComponents/DataComponents.stories.tsx
new file mode 100644
index 0000000000..83ca7a8257
--- /dev/null
+++ b/packages/odyssey-storybook/src/components/odyssey-labs/DataComponents/DataComponents.stories.tsx
@@ -0,0 +1,565 @@
+/*!
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import type { Meta, StoryObj } from "@storybook/react";
+import {
+ DataRow,
+ DataView,
+ DataViewProps,
+ DataGetDataType,
+ DataOnReorderRowsType,
+ DataRowSelectionState,
+ densityValues,
+ availableLayouts,
+ TableProps,
+ StackProps,
+ DataTable,
+ DataStack,
+} from "@okta/odyssey-react-mui/labs";
+import { PauseIcon, RefreshIcon } from "@okta/odyssey-react-mui/icons";
+import { MuiThemeDecorator } from "../../../../.storybook/components";
+import {
+ Person,
+ columns as personColumns,
+ data as personData,
+} from "./personData";
+import { filterData, reorderData } from "./dataFunctions";
+import { Dispatch, SetStateAction, useCallback, useState } from "react";
+import {
+ Box,
+ MenuItem,
+ Status,
+ Button,
+ EmptyState,
+ paginationTypeValues,
+} from "@okta/odyssey-react-mui";
+
+type DataViewMetaProps = DataViewProps &
+ TableProps &
+ StackProps & {
+ tableRowActionMenuItems: TableProps["rowActionMenuItems"];
+ stackRowActionMenuItems: StackProps["rowActionMenuItems"];
+ hasCustomEmptyPlaceholder: boolean;
+ hasCustomNoResultsPlaceholder: boolean;
+ hasActionMenuItems: boolean;
+ hasActionButtons: boolean;
+ };
+
+const storybookMeta: Meta = {
+ title: "Labs Components/Data components",
+ component: DataView,
+ argTypes: {
+ getData: {
+ control: null,
+ table: {
+ type: {
+ summary: "",
+ },
+ },
+ },
+ getRowId: {
+ control: null,
+ table: {
+ type: {
+ summary: "",
+ },
+ },
+ },
+ hasRowReordering: {
+ control: "boolean",
+ table: {
+ type: {
+ summary: "",
+ },
+ },
+ },
+ isRowReorderingDisabled: {
+ control: "boolean",
+ },
+ onReorderRows: {
+ control: null,
+ table: {
+ type: {
+ summary: "",
+ },
+ },
+ },
+ hasRowSelection: {
+ control: "boolean",
+ },
+ onChangeRowSelection: {
+ control: null,
+ },
+ bulkActionMenuItems: {
+ control: null,
+ },
+ hasPagination: {
+ control: "boolean",
+ },
+ currentPage: {
+ control: "number",
+ },
+ paginationType: {
+ control: "select",
+ options: paginationTypeValues,
+ },
+ resultsPerPage: {
+ control: "number",
+ },
+ totalRows: {
+ control: "number",
+ },
+ hasFilters: {
+ control: "boolean",
+ },
+ hasSearch: {
+ control: "boolean",
+ },
+ hasSearchSubmitButton: {
+ control: "boolean",
+ },
+ filters: {
+ control: null,
+ },
+ searchDelayTime: {
+ control: "number",
+ },
+ errorMessage: {
+ control: "text",
+ },
+ emptyPlaceholder: {
+ control: null,
+ },
+ noResultsPlaceholder: {
+ control: null,
+ },
+ initialLayout: {
+ control: "select",
+ options: availableLayouts,
+ },
+ availableLayouts: {
+ control: "check",
+ options: availableLayouts,
+ },
+ columns: {
+ control: null,
+ name: "tableOptions.columns",
+ },
+ initialDensity: {
+ control: "select",
+ options: densityValues,
+ name: "tableOptions.columns",
+ },
+ hasChangeableDensity: {
+ control: "boolean",
+ name: "tableOptions.hasChangeableDensity",
+ },
+ hasColumnResizing: {
+ control: "boolean",
+ name: "tableOptions.hasColumnResizing",
+ },
+ hasColumnVisibility: {
+ control: "boolean",
+ name: "tableOptions.hasColumnVisibility",
+ },
+ renderDetailPanel: {
+ control: null,
+ name: "tableOptions.renderDetailPanel",
+ },
+ rowActionButtons: {
+ control: null,
+ name: "tableOptions.rowActionButtons",
+ },
+ tableRowActionMenuItems: {
+ control: null,
+ name: "tableOptions.rowActionMenuItems",
+ },
+ hasSorting: {
+ control: "boolean",
+ name: "tableOptions.hasSorting",
+ },
+ cardProps: {
+ control: null,
+ name: "stackOptions.cardProps",
+ },
+ maxGridColumns: {
+ control: "number",
+ name: "stackOptions.maxGridColumns",
+ },
+ stackRowActionMenuItems: {
+ control: null,
+ name: "stackOptions.rowActionMenuItems",
+ },
+ isLoading: {
+ control: "boolean",
+ },
+ isEmpty: {
+ control: "boolean",
+ },
+ isNoResults: {
+ control: "boolean",
+ },
+ hasCustomEmptyPlaceholder: {
+ control: "boolean",
+ name: "[STORY ONLY] Has custom empty placeholder?",
+ },
+ hasCustomNoResultsPlaceholder: {
+ control: "boolean",
+ name: "[STORY ONLY] Has custom 'no results' placeholder?",
+ },
+ hasActionMenuItems: {
+ control: "boolean",
+ name: "[STORY ONLY] Has action menu items?",
+ },
+ hasActionButtons: {
+ control: "boolean",
+ name: "[STORY ONLY] Has action buttons in table view?",
+ },
+ },
+ args: {
+ currentPage: 1,
+ resultsPerPage: 20,
+ paginationType: "paged",
+ maxGridColumns: 3,
+ hasActionButtons: false,
+ hasActionMenuItems: false,
+ hasCustomEmptyPlaceholder: false,
+ hasCustomNoResultsPlaceholder: false,
+ availableLayouts: ["table", "stack", "grid"],
+ initialLayout: "table",
+ },
+ decorators: [MuiThemeDecorator],
+};
+
+export default storybookMeta;
+
+const useDataCallbacks = (
+ data: Person[],
+ setData: Dispatch>,
+) => {
+ const getData = useCallback(
+ ({ ...props }: DataGetDataType) => {
+ return filterData({ data, ...props });
+ },
+ [data],
+ );
+
+ const onReorderRows = useCallback(
+ ({ ...props }: DataOnReorderRowsType) => {
+ const reorderedData = reorderData({ data, ...props });
+ setData(reorderedData);
+ },
+ [data, setData],
+ );
+
+ const onChangeRowSelection = useCallback(
+ (rowSelection: DataRowSelectionState) => {
+ if (Object.keys(rowSelection).length > 0) {
+ console.log(`${Object.keys(rowSelection).length} selected`);
+ }
+ },
+ [],
+ );
+
+ return { getData, onReorderRows, onChangeRowSelection };
+};
+
+// Common action menu items
+const actionMenuItems = (selectedRows: DataRowSelectionState) => (
+ <>
+
+
+ >
+);
+
+const actionButtons = () => (
+
+ }
+ ariaLabel="Pause"
+ size="small"
+ variant="floating"
+ />
+ }
+ ariaLabel="Refresh"
+ size="small"
+ variant="floating"
+ />
+
+);
+
+const customEmptyPlaceholder = (
+ }
+ SecondaryCallToActionComponent={
+
+ }
+ />
+);
+
+const customNoResultsPlaceholder = (
+
+);
+
+const cardProps = (row: DataRow) => ({
+ overline: `${row.city}, ${row.state}`,
+ title: row.name,
+ description: `${row.name} is ${row.age} years old.`,
+ children: (
+
+ ),
+});
+
+// Base story configuration
+const BaseStory: StoryObj = {
+ render: function Base(args) {
+ const [data, setData] = useState(personData);
+ const { getData, onReorderRows, onChangeRowSelection } = useDataCallbacks(
+ data,
+ setData,
+ );
+
+ return (
+
+ );
+ },
+};
+
+export const Default: StoryObj = {
+ ...BaseStory,
+ args: {},
+};
+
+export const TableOnly: StoryObj = {
+ ...BaseStory,
+ args: {
+ availableLayouts: ["table"],
+ },
+};
+
+export const StackWithMultipleLayouts: StoryObj = {
+ ...BaseStory,
+ args: {
+ availableLayouts: ["list", "grid"],
+ },
+};
+
+export const StackListOnly: StoryObj = {
+ ...BaseStory,
+ args: {
+ availableLayouts: ["list"],
+ },
+};
+
+export const StackGridOnly: StoryObj = {
+ ...BaseStory,
+ args: {
+ availableLayouts: ["grid"],
+ },
+};
+
+export const Everything: StoryObj = {
+ ...BaseStory,
+ args: {
+ hasRowReordering: true,
+ hasPagination: true,
+ hasFilters: true,
+ hasSearch: true,
+ hasChangeableDensity: true,
+ hasColumnResizing: true,
+ hasColumnVisibility: true,
+ hasSorting: true,
+ hasActionButtons: true,
+ hasActionMenuItems: true,
+ hasRowSelection: true,
+ },
+};
+
+export const DataTableComponent: StoryObj = {
+ render: function Base(args) {
+ const [data, setData] = useState(personData);
+ const { getData, onReorderRows, onChangeRowSelection } = useDataCallbacks(
+ data,
+ setData,
+ );
+
+ return (
+
+ );
+ },
+};
+
+export const DataStackComponent: StoryObj = {
+ render: function Base(args) {
+ const [data, setData] = useState(personData);
+ const { getData, onReorderRows, onChangeRowSelection } = useDataCallbacks(
+ data,
+ setData,
+ );
+
+ return (
+
+ );
+ },
+};
diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/DataComponents/dataFunctions.ts b/packages/odyssey-storybook/src/components/odyssey-labs/DataComponents/dataFunctions.ts
new file mode 100644
index 0000000000..557dffb308
--- /dev/null
+++ b/packages/odyssey-storybook/src/components/odyssey-labs/DataComponents/dataFunctions.ts
@@ -0,0 +1,121 @@
+/*!
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+// import {
+// DataTableGetDataType,
+// DataTableOnReorderRowsType,
+// } from "@okta/odyssey-react-mui";
+import {
+ DataGetDataType,
+ DataOnReorderRowsType,
+} from "@okta/odyssey-react-mui/labs";
+import { Person } from "./personData";
+
+export const filterData = ({
+ data,
+ ...args
+}: {
+ data: Person[];
+} & DataGetDataType) => {
+ let filteredData = data;
+ const { search, filters, sort, page = 1, resultsPerPage = 20 } = args;
+
+ // Implement text-based query filtering
+ if (search) {
+ filteredData = filteredData.filter((row) =>
+ Object.values(row).some((value) =>
+ value.toString().toLowerCase().includes(search.toLowerCase()),
+ ),
+ );
+ }
+
+ // Implement column-specific filtering
+ if (filters) {
+ filteredData = filteredData.filter((row) => {
+ return filters.every(({ id, value }) => {
+ // If filter value is null or undefined, skip this filter
+ if (value === null || value === undefined) {
+ return true;
+ }
+
+ // If filter value is array, search for each array value
+ if (Array.isArray(value)) {
+ return value.some((arrayValue) => {
+ return row[id as keyof Person]
+ ?.toString()
+ .toLowerCase()
+ .includes(arrayValue.toString().toLowerCase());
+ });
+ }
+
+ // In the custom filter examples, we provide a "starting letter"
+ // control that allows the user to filter by whether the
+ // first letter is a vowel or consonant
+ if (id === "startLetter" && typeof row.name === "string") {
+ const firstLetter = row.name[0]?.toLowerCase();
+ if (value === "vowel") return "aeiou".includes(firstLetter);
+ if (value === "consonant") return !"aeiou".includes(firstLetter);
+ return true;
+ }
+
+ // General filtering for other columns
+ return row[id as keyof Person]
+ ?.toString()
+ .toLowerCase()
+ .includes(value.toString().toLowerCase());
+ });
+ });
+ }
+
+ // Implement sorting
+ if (sort && sort.length > 0) {
+ filteredData.sort((a, b) => {
+ for (const { id, desc } of sort) {
+ const aValue = a[id as keyof Person];
+ const bValue = b[id as keyof Person];
+
+ if (aValue < bValue) return desc ? 1 : -1;
+ if (aValue > bValue) return desc ? -1 : 1;
+ }
+
+ return 0;
+ });
+ }
+
+ // Implement pagination
+ const startRow = (page - 1) * resultsPerPage;
+ const endRow = startRow + resultsPerPage;
+ filteredData = filteredData.slice(startRow, endRow);
+
+ return filteredData;
+};
+
+export const reorderData = ({
+ data,
+ ...args
+}: {
+ data: T[];
+} & DataOnReorderRowsType) => {
+ const updatedData = data;
+ const { rowId, newRowIndex } = args;
+ const rowIndex = updatedData.findIndex((row) => row.id === rowId);
+
+ if (rowIndex !== -1) {
+ // Remove the row from its current position
+ const [removedRow] = updatedData.splice(rowIndex, 1);
+
+ // Insert the row at the new index
+ updatedData.splice(newRowIndex, 0, removedRow);
+ }
+
+ return updatedData;
+};
diff --git a/packages/odyssey-storybook/src/components/odyssey-labs/DataComponents/personData.tsx b/packages/odyssey-storybook/src/components/odyssey-labs/DataComponents/personData.tsx
new file mode 100644
index 0000000000..e8d8fe78de
--- /dev/null
+++ b/packages/odyssey-storybook/src/components/odyssey-labs/DataComponents/personData.tsx
@@ -0,0 +1,1885 @@
+/*!
+ * Copyright (c) 2023-present, Okta, Inc. and/or its affiliates. All rights reserved.
+ * The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (the "License.")
+ *
+ * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *
+ * See the License for the specific language governing permissions and limitations under the License.
+ */
+
+import { Status } from "@okta/odyssey-react-mui";
+import { DataColumns } from "@okta/odyssey-react-mui/labs";
+
+export type Person = {
+ order: number;
+ id: string;
+ name: string;
+ city: string;
+ state: string;
+ age: number;
+ risk: "high" | "medium" | "low";
+};
+
+export const columns: DataColumns = [
+ {
+ accessorKey: "order",
+ header: "ID",
+ enableColumnFilter: false,
+ size: 120,
+ },
+ {
+ accessorKey: "name",
+ header: "Name",
+ enableHiding: false,
+ },
+ {
+ accessorKey: "city",
+ header: "City",
+ },
+ {
+ accessorKey: "state",
+ header: "State",
+ },
+ {
+ accessorKey: "age",
+ header: "Age",
+ size: 80,
+ filterVariant: "range",
+ },
+ {
+ accessorKey: "risk",
+ header: "Risk level",
+ Cell: ({ cell }) => {
+ const value = cell.getValue();
+ const severity =
+ value === "low" ? "success" : value === "medium" ? "warning" : "error";
+ return (
+
+ );
+ },
+ filterVariant: "multi-select",
+ filterSelectOptions: [
+ {
+ label: "Low",
+ value: "low",
+ },
+ {
+ label: "Medium",
+ value: "medium",
+ },
+ {
+ label: "High",
+ value: "high",
+ },
+ ],
+ },
+];
+
+export const data: Person[] = [
+ {
+ order: 0,
+ id: "9d3cd264-0212-4827-b421-3d77bc22c0a9",
+ name: "John Doe",
+ city: "New Paltz",
+ state: "New York",
+ age: 34,
+ risk: "high",
+ },
+ {
+ order: 1,
+ id: "bf663960-2df9-42c4-8cab-ac95f8c9b04e",
+ name: "Cecil Steuber",
+ city: "New Arlo",
+ state: "Florida",
+ age: 95,
+ risk: "low",
+ },
+ {
+ order: 2,
+ id: "f8046c00-fe48-421e-bd7b-5a5afc63aa61",
+ name: "Lionel Lynch",
+ city: "Kirlinboro",
+ state: "Kansas",
+ age: 97,
+ risk: "high",
+ },
+ {
+ order: 3,
+ id: "8051ea50-285e-45b3-8172-6ea1e2dd0da4",
+ name: "Lamar Bayer",
+ city: "West Kayleetown",
+ state: "New Jersey",
+ age: 90,
+ risk: "medium",
+ },
+ {
+ order: 4,
+ id: "d93f395b-8958-4ef7-af34-0f4ccbdfa042",
+ name: "Mr. Dwayne Howe II",
+ city: "Fort Alvisstead",
+ state: "Pennsylvania",
+ age: 23,
+ risk: "low",
+ },
+ {
+ order: 5,
+ id: "b86cd0e4-dca1-4957-b844-ff976a6394f8",
+ name: "Valerie Klein",
+ city: "West Orenview",
+ state: "Idaho",
+ age: 91,
+ risk: "medium",
+ },
+ {
+ order: 6,
+ id: "086fa172-6d44-49ef-8406-6b34cccad694",
+ name: "Randal Howe",
+ city: "New Loganton",
+ state: "Arizona",
+ age: 26,
+ risk: "medium",
+ },
+ {
+ order: 7,
+ id: "ad806feb-44c9-4e21-bd41-5c3c76090657",
+ name: "Herbert Stanton",
+ city: "Katrinefurt",
+ state: "Indiana",
+ age: 20,
+ risk: "high",
+ },
+ {
+ order: 8,
+ id: "9cc74183-214d-4cca-b948-82e5fe27bb74",
+ name: "Kyle Hoeger",
+ city: "North Kodyborough",
+ state: "Virginia",
+ age: 86,
+ risk: "high",
+ },
+ {
+ order: 9,
+ id: "273e72d4-25d8-4c53-9a51-36e43fc75524",
+ name: "Nina Carroll",
+ city: "West Caleigh",
+ state: "Missouri",
+ age: 88,
+ risk: "high",
+ },
+ {
+ order: 10,
+ id: "62b86ff6-003e-4dd0-aedf-c012b1de1eb0",
+ name: "Moses Glover",
+ city: "Berkeley",
+ state: "South Dakota",
+ age: 79,
+ risk: "low",
+ },
+ {
+ order: 11,
+ id: "08fac7bf-ac9c-4b8b-8ede-f64c946a1fb6",
+ name: "Mr. Cesar Cole",
+ city: "Lake Kailey",
+ state: "Montana",
+ age: 32,
+ risk: "low",
+ },
+ {
+ order: 12,
+ id: "bc082c2c-2953-40b6-aeb1-110daeef81bf",
+ name: "Dixie Kuphal IV",
+ city: "East Esmeralda",
+ state: "Missouri",
+ age: 92,
+ risk: "low",
+ },
+ {
+ order: 13,
+ id: "ec955b41-a9c1-4092-bdae-c8daecc67392",
+ name: "Sylvester Gislason V",
+ city: "East Johnnybury",
+ state: "Missouri",
+ age: 35,
+ risk: "high",
+ },
+ {
+ order: 14,
+ id: "0cd8c723-078b-45c3-8a49-824a74ddba46",
+ name: "Margaret Toy DDS",
+ city: "Rubenstead",
+ state: "Minnesota",
+ age: 36,
+ risk: "medium",
+ },
+ {
+ order: 15,
+ id: "a4e4b630-76cb-4797-a97d-8c7c93b30f03",
+ name: "Elena Haag",
+ city: "East Ernie",
+ state: "Texas",
+ age: 75,
+ risk: "high",
+ },
+ {
+ order: 16,
+ id: "4c3884f5-a447-46a3-89a9-2b0ab2373aa2",
+ name: "Pamela Effertz",
+ city: "West Jamarcusboro",
+ state: "Arkansas",
+ age: 43,
+ risk: "low",
+ },
+ {
+ order: 17,
+ id: "309fb05e-583d-4201-b334-e6d9f4ae74e6",
+ name: "Mrs. Gloria Douglas",
+ city: "Cummingshaven",
+ state: "Ohio",
+ age: 32,
+ risk: "medium",
+ },
+ {
+ order: 18,
+ id: "b952cf04-8182-4da0-8671-9fe8a0c42e16",
+ name: "Tommie Schneider",
+ city: "Lake Lolitaview",
+ state: "Michigan",
+ age: 47,
+ risk: "medium",
+ },
+ {
+ order: 19,
+ id: "6de5bb9c-7abb-4995-ace5-0ac9eeed13a7",
+ name: "Edna Kling",
+ city: "Port Jimmyborough",
+ state: "Georgia",
+ age: 23,
+ risk: "medium",
+ },
+ {
+ order: 20,
+ id: "2c129486-6dda-4391-976b-b41021473308",
+ name: "Gordon Jast",
+ city: "Prosaccoborough",
+ state: "New Mexico",
+ age: 99,
+ risk: "low",
+ },
+ {
+ order: 21,
+ id: "ffd2953d-a49a-48d0-89ef-903fd05774ad",
+ name: "Salvador Armstrong",
+ city: "Alysaton",
+ state: "Nevada",
+ age: 65,
+ risk: "medium",
+ },
+ {
+ order: 22,
+ id: "e5ab25bc-60c3-4888-9f3e-ec66fbe9f2aa",
+ name: "Sheila Harris",
+ city: "South Allenton",
+ state: "Tennessee",
+ age: 46,
+ risk: "low",
+ },
+ {
+ order: 23,
+ id: "48069214-aaf6-4bb1-8a74-314ba899c245",
+ name: "Rochelle Rohan",
+ city: "New Coryside",
+ state: "Oklahoma",
+ age: 98,
+ risk: "medium",
+ },
+ {
+ order: 24,
+ id: "95adb05b-dd7b-4f38-8434-1fc65fadc096",
+ name: "Rodney White",
+ city: "North Jonathonshire",
+ state: "Texas",
+ age: 47,
+ risk: "high",
+ },
+ {
+ order: 25,
+ id: "bd58c1d4-dc60-4d03-8fd1-96cd42f56272",
+ name: "Faith Kub",
+ city: "Morissetteboro",
+ state: "New Hampshire",
+ age: 87,
+ risk: "low",
+ },
+ {
+ order: 26,
+ id: "1feebd5f-16de-456b-8196-d1b2bd54b633",
+ name: "Mrs. Vera Brown",
+ city: "East Watson",
+ state: "Utah",
+ age: 27,
+ risk: "high",
+ },
+ {
+ order: 27,
+ id: "25a8afd9-e34a-4ef4-8a14-714c6d72e449",
+ name: "Gregory Dare",
+ city: "East Ramona",
+ state: "Indiana",
+ age: 65,
+ risk: "medium",
+ },
+ {
+ order: 28,
+ id: "e43c16e7-9830-4e68-b492-27abe3e30966",
+ name: "Charlie Steuber-Weber",
+ city: "Toniview",
+ state: "Idaho",
+ age: 77,
+ risk: "medium",
+ },
+ {
+ order: 29,
+ id: "0cd47890-0cd5-4db2-b74d-1e450c49a985",
+ name: "Jimmie Dicki-Stiedemann",
+ city: "Purdyfort",
+ state: "Wyoming",
+ age: 29,
+ risk: "low",
+ },
+ {
+ order: 30,
+ id: "82bb23af-5378-4d20-b266-a1f2ed557d7b",
+ name: "Ralph Hane DDS",
+ city: "Antonettehaven",
+ state: "Pennsylvania",
+ age: 93,
+ risk: "low",
+ },
+ {
+ order: 31,
+ id: "da195eac-8e19-4b22-8185-afffe9e8d308",
+ name: "Doreen Breitenberg",
+ city: "North Margretton",
+ state: "Michigan",
+ age: 83,
+ risk: "medium",
+ },
+ {
+ order: 32,
+ id: "f0b17830-686d-4d32-98ca-f37c4de0eb39",
+ name: "Mr. Guy Jacobson",
+ city: "East Kaelyn",
+ state: "Hawaii",
+ age: 28,
+ risk: "low",
+ },
+ {
+ order: 33,
+ id: "7db48194-3a13-4453-971d-9eecb5c29909",
+ name: "Nathaniel Conroy",
+ city: "Beckerton",
+ state: "California",
+ age: 94,
+ risk: "low",
+ },
+ {
+ order: 34,
+ id: "7f38910b-e4c6-4d16-96d6-ce2c3f7875fa",
+ name: "Hope Klocko III",
+ city: "Lincolnstead",
+ state: "Colorado",
+ age: 52,
+ risk: "low",
+ },
+ {
+ order: 35,
+ id: "08dc854c-def6-4a02-a6ec-e2afdd455f8c",
+ name: "Mrs. Valerie Raynor",
+ city: "Kevenview",
+ state: "Indiana",
+ age: 81,
+ risk: "high",
+ },
+ {
+ order: 36,
+ id: "e1ba2b2d-1468-4e65-877c-ae59bc7aea44",
+ name: "Tracey Hammes",
+ city: "New Harveyburgh",
+ state: "North Carolina",
+ age: 22,
+ risk: "low",
+ },
+ {
+ order: 37,
+ id: "f5b7d6f4-9dcd-4f28-b163-7cbaa99b7723",
+ name: "Nina Paucek",
+ city: "Port Reta",
+ state: "New Hampshire",
+ age: 53,
+ risk: "low",
+ },
+ {
+ order: 38,
+ id: "ed9ebc5a-f104-42ca-b9a6-dbcbd6bbf27d",
+ name: "Derrick Hodkiewicz",
+ city: "Port Bettiebury",
+ state: "Maine",
+ age: 82,
+ risk: "low",
+ },
+ {
+ order: 39,
+ id: "2562adc7-a956-452c-8a51-6997442d0dca",
+ name: "Maggie Orn",
+ city: "Stromanbury",
+ state: "Vermont",
+ age: 36,
+ risk: "low",
+ },
+ {
+ order: 40,
+ id: "19606ba5-2a05-4a84-98fd-bdfeac306110",
+ name: "Jeanne Borer",
+ city: "Fort Coltenfurt",
+ state: "Pennsylvania",
+ age: 26,
+ risk: "medium",
+ },
+ {
+ order: 41,
+ id: "d8ba8b5f-7a4d-4f77-8ce8-c00255038226",
+ name: "Suzanne Daniel",
+ city: "Morissetteworth",
+ state: "Michigan",
+ age: 58,
+ risk: "medium",
+ },
+ {
+ order: 42,
+ id: "49345933-6a7e-47b2-a280-0517fdb0ca17",
+ name: "Lola Kautzer",
+ city: "New Destinystad",
+ state: "Colorado",
+ age: 22,
+ risk: "medium",
+ },
+ {
+ order: 43,
+ id: "1f397994-d931-4cf4-a9e1-d2378c67c4cd",
+ name: "Mabel Bernhard DVM",
+ city: "Howestad",
+ state: "Washington",
+ age: 46,
+ risk: "high",
+ },
+ {
+ order: 44,
+ id: "1406f3b1-b93f-491e-a41c-c4b17b7ead97",
+ name: "Tyler Dare",
+ city: "Boulder",
+ state: "Minnesota",
+ age: 81,
+ risk: "medium",
+ },
+ {
+ order: 45,
+ id: "a4ffca1f-cc28-441c-b25a-da24df4f3651",
+ name: "Noah Jacobi",
+ city: "New Marianefield",
+ state: "Delaware",
+ age: 67,
+ risk: "low",
+ },
+ {
+ order: 46,
+ id: "ac9a2226-13c7-429b-b941-0297b3730a7a",
+ name: "Danny Cormier",
+ city: "Florissant",
+ state: "Massachusetts",
+ age: 88,
+ risk: "high",
+ },
+ {
+ order: 47,
+ id: "ef9e1979-657c-46ad-81d2-b94f4dff3d4f",
+ name: "Alex Kautzer",
+ city: "West Bettyfield",
+ state: "Illinois",
+ age: 47,
+ risk: "high",
+ },
+ {
+ order: 48,
+ id: "d7feab4a-6055-48aa-a0f1-cedc3c002067",
+ name: "Jared Hermann",
+ city: "Fort Audraborough",
+ state: "Washington",
+ age: 63,
+ risk: "high",
+ },
+ {
+ order: 49,
+ id: "e0580323-fb6e-4350-84ff-6d6bec51b4e2",
+ name: "Irene Luettgen Jr.",
+ city: "Louisville/Jefferson County",
+ state: "Arkansas",
+ age: 26,
+ risk: "low",
+ },
+ {
+ order: 50,
+ id: "e51fef7f-b8c1-41b4-875d-a65e30b85ea7",
+ name: "Gerald VonRueden",
+ city: "New Jaimeton",
+ state: "Vermont",
+ age: 50,
+ risk: "low",
+ },
+ {
+ order: 51,
+ id: "38babdf7-20c3-45fe-8b84-45a8a382c722",
+ name: "Raquel D'Amore",
+ city: "South Astridmouth",
+ state: "New York",
+ age: 49,
+ risk: "high",
+ },
+ {
+ order: 52,
+ id: "3f3c4e94-7155-4b1e-a351-8d6e1d18a187",
+ name: "Ms. Amy Collins",
+ city: "West Sanford",
+ state: "Maine",
+ age: 54,
+ risk: "low",
+ },
+ {
+ order: 53,
+ id: "a969f0d4-9847-4ccc-a03c-d1b316405b41",
+ name: "Marilyn Vandervort IV",
+ city: "Samsonborough",
+ state: "Montana",
+ age: 58,
+ risk: "medium",
+ },
+ {
+ order: 54,
+ id: "db056e8b-ca7c-4cef-8ae0-c05a0930a9e2",
+ name: "Ms. Kristin Okuneva",
+ city: "New Wilma",
+ state: "South Carolina",
+ age: 95,
+ risk: "medium",
+ },
+ {
+ order: 55,
+ id: "37cca6e0-d853-4ce5-a6d5-0591dceb0f31",
+ name: "Nicole Hane",
+ city: "Boehmfort",
+ state: "Iowa",
+ age: 23,
+ risk: "medium",
+ },
+ {
+ order: 56,
+ id: "1222feac-64df-4335-a462-817721366a16",
+ name: "Marian Kutch",
+ city: "Kimfort",
+ state: "Vermont",
+ age: 69,
+ risk: "high",
+ },
+ {
+ order: 57,
+ id: "de7d9cf8-3b4c-4268-9922-f644e4fcbbac",
+ name: "Mr. Cary Larson",
+ city: "Noahbury",
+ state: "Washington",
+ age: 46,
+ risk: "high",
+ },
+ {
+ order: 58,
+ id: "084b433d-84e4-4929-8fd6-ac1c47a235d3",
+ name: "Felicia Jones-Fritsch",
+ city: "Mandyport",
+ state: "Washington",
+ age: 89,
+ risk: "high",
+ },
+ {
+ order: 59,
+ id: "8d4acef0-67b5-4862-8d41-65dea98f8bd5",
+ name: "Ruth Koepp",
+ city: "Cedar Hill",
+ state: "North Carolina",
+ age: 97,
+ risk: "high",
+ },
+ {
+ order: 60,
+ id: "5dee93b7-b9b6-4f30-befa-b3c124ac884c",
+ name: "Lori Windler",
+ city: "North Denafield",
+ state: "Maryland",
+ age: 72,
+ risk: "high",
+ },
+ {
+ order: 61,
+ id: "62655293-0b67-4574-b63d-bcff4d8ca919",
+ name: "Marion Terry",
+ city: "Isadorestad",
+ state: "Kentucky",
+ age: 32,
+ risk: "medium",
+ },
+ {
+ order: 62,
+ id: "23b04edb-ea32-4586-acec-03d345ee0b06",
+ name: "Roosevelt Walsh",
+ city: "North Fausto",
+ state: "Illinois",
+ age: 95,
+ risk: "low",
+ },
+ {
+ order: 63,
+ id: "c450a75d-8ef2-4770-89ac-f36b686b7894",
+ name: "Mrs. Vivian Bernhard",
+ city: "New Emily",
+ state: "Oregon",
+ age: 27,
+ risk: "high",
+ },
+ {
+ order: 64,
+ id: "a0f79b46-58ce-4faa-af4d-d5052170f1ab",
+ name: "Benjamin Ryan",
+ city: "Bartellville",
+ state: "Mississippi",
+ age: 30,
+ risk: "medium",
+ },
+ {
+ order: 65,
+ id: "418fe2a4-3574-4749-967c-58b9d073111d",
+ name: "Alison Mohr",
+ city: "San Angelo",
+ state: "Idaho",
+ age: 40,
+ risk: "high",
+ },
+ {
+ order: 66,
+ id: "3d702da1-0c09-4dae-b70a-3ce92a2069e9",
+ name: "Gustavo Champlin",
+ city: "Quitzonboro",
+ state: "Utah",
+ age: 40,
+ risk: "low",
+ },
+ {
+ order: 67,
+ id: "70097612-2c8a-4ea0-9768-456dd6b0aa26",
+ name: "Roxanne Bechtelar",
+ city: "Yeseniaville",
+ state: "Washington",
+ age: 32,
+ risk: "low",
+ },
+ {
+ order: 68,
+ id: "e1ae0aa8-e4b4-434b-9bf9-ca935590021a",
+ name: "Ora Kutch",
+ city: "South Rethafort",
+ state: "Colorado",
+ age: 61,
+ risk: "high",
+ },
+ {
+ order: 69,
+ id: "43f9156d-0124-4d98-bed9-ff9d635e5154",
+ name: "Toni Fritsch",
+ city: "Fort Lawson",
+ state: "West Virginia",
+ age: 22,
+ risk: "low",
+ },
+ {
+ order: 70,
+ id: "107a4c47-1b0c-4203-bf9c-7e610d62e9f3",
+ name: "Kerry Hansen",
+ city: "Kuphalmouth",
+ state: "Massachusetts",
+ age: 64,
+ risk: "high",
+ },
+ {
+ order: 71,
+ id: "1c55f297-aeea-490a-ab47-f4b9d5956ae2",
+ name: "Brandi Ledner",
+ city: "West Jedediahville",
+ state: "Maryland",
+ age: 54,
+ risk: "low",
+ },
+ {
+ order: 72,
+ id: "da759266-6398-48fc-a89c-e43584f39dce",
+ name: "Natalie Wisozk",
+ city: "Maxinestead",
+ state: "Virginia",
+ age: 35,
+ risk: "low",
+ },
+ {
+ order: 73,
+ id: "1c7e2ce5-3de6-4e49-8fe4-5c0b5bcbd9cc",
+ name: "Dorothy Botsford",
+ city: "Frederikcester",
+ state: "Maine",
+ age: 97,
+ risk: "medium",
+ },
+ {
+ order: 74,
+ id: "6f29d2b9-3a68-4879-83fd-e29c643f0f9d",
+ name: "Angela Conn",
+ city: "South Norafort",
+ state: "Hawaii",
+ age: 52,
+ risk: "high",
+ },
+ {
+ order: 75,
+ id: "250652ad-e8c4-45f6-944e-e52e367bdc38",
+ name: "Roy Walker Jr.",
+ city: "Wilkinsoncester",
+ state: "Louisiana",
+ age: 26,
+ risk: "low",
+ },
+ {
+ order: 76,
+ id: "e78c8b9e-5167-43ab-97fc-8f6d817acb7a",
+ name: "Kristy Nicolas",
+ city: "Stoltenbergshire",
+ state: "North Dakota",
+ age: 57,
+ risk: "high",
+ },
+ {
+ order: 77,
+ id: "feb37af6-c57f-4d20-9521-e0265798f238",
+ name: "Gordon Cremin",
+ city: "Cummerataville",
+ state: "Kentucky",
+ age: 70,
+ risk: "high",
+ },
+ {
+ order: 78,
+ id: "36c3e293-ebb2-4826-ab8c-f244ed7283b3",
+ name: "Wilma Orn",
+ city: "Pacochabury",
+ state: "South Carolina",
+ age: 46,
+ risk: "medium",
+ },
+ {
+ order: 79,
+ id: "03e2b3f2-56ca-463e-ae7b-cd0f7a535957",
+ name: "Nicolas Trantow",
+ city: "North Onie",
+ state: "Oklahoma",
+ age: 47,
+ risk: "medium",
+ },
+ {
+ order: 80,
+ id: "f6864b9c-fadc-40db-9144-be5a6ed5f50b",
+ name: "Jesse Rippin",
+ city: "Killeen",
+ state: "Kansas",
+ age: 82,
+ risk: "medium",
+ },
+ {
+ order: 81,
+ id: "d5555bee-247a-4b15-a747-33c4f4ffe157",
+ name: "Nina Cruickshank V",
+ city: "East Tristian",
+ state: "Connecticut",
+ age: 83,
+ risk: "low",
+ },
+ {
+ order: 82,
+ id: "05d30dbd-267d-439a-a831-d247fbf6fde1",
+ name: "Sherman Schmidt",
+ city: "Port Lucas",
+ state: "Maryland",
+ age: 88,
+ risk: "low",
+ },
+ {
+ order: 83,
+ id: "c2f6d838-8c65-4670-abb8-9594ad03faa6",
+ name: "Dr. Gregory Walsh",
+ city: "South Devonte",
+ state: "Idaho",
+ age: 77,
+ risk: "low",
+ },
+ {
+ order: 84,
+ id: "c2041318-610d-47bc-86f9-09610f786612",
+ name: "Doreen Schimmel",
+ city: "Fort Collins",
+ state: "Iowa",
+ age: 48,
+ risk: "medium",
+ },
+ {
+ order: 85,
+ id: "09e4079c-5b8b-4522-a159-35768563f35e",
+ name: "Wayne Morar",
+ city: "Fort Rosemarie",
+ state: "Montana",
+ age: 19,
+ risk: "low",
+ },
+ {
+ order: 86,
+ id: "b1b9f5e9-ec96-43be-92e3-17bfb4bb4d56",
+ name: "Mr. Jordan Schroeder",
+ city: "North Jessicaboro",
+ state: "New Mexico",
+ age: 25,
+ risk: "medium",
+ },
+ {
+ order: 87,
+ id: "b34de79f-d906-4da2-ba32-8b4a38596301",
+ name: "Dr. Benny Schuppe",
+ city: "Gordonburgh",
+ state: "Florida",
+ age: 91,
+ risk: "high",
+ },
+ {
+ order: 88,
+ id: "ffe99215-3a55-4b65-ab4d-9e1bc893dc7d",
+ name: "Madeline Hahn",
+ city: "West Elise",
+ state: "West Virginia",
+ age: 73,
+ risk: "high",
+ },
+ {
+ order: 89,
+ id: "d6ca49a9-fd28-4edb-97ad-06b3a39f9196",
+ name: "Hugo Cole",
+ city: "Fairfield",
+ state: "Kentucky",
+ age: 38,
+ risk: "high",
+ },
+ {
+ order: 90,
+ id: "21a31b30-e125-4d68-81b9-deb83f147866",
+ name: "Jackie Prosacco",
+ city: "North Madie",
+ state: "New York",
+ age: 26,
+ risk: "medium",
+ },
+ {
+ order: 91,
+ id: "d5d75640-68d5-48c3-8fa5-48c326dab373",
+ name: "Dr. Ada Moen",
+ city: "Rowlett",
+ state: "Maine",
+ age: 18,
+ risk: "high",
+ },
+ {
+ order: 92,
+ id: "12228239-8d2f-4284-a84a-57fab4712f34",
+ name: "Shelley Schowalter DVM",
+ city: "South Mauricio",
+ state: "Maryland",
+ age: 55,
+ risk: "high",
+ },
+ {
+ order: 93,
+ id: "4fff095f-12a5-4831-8d31-67a92d29bb26",
+ name: "David Little",
+ city: "Strackechester",
+ state: "Mississippi",
+ age: 46,
+ risk: "high",
+ },
+ {
+ order: 94,
+ id: "d789882d-1879-422b-951f-5fc8179de402",
+ name: "Tanya Koepp IV",
+ city: "East Rae",
+ state: "Kentucky",
+ age: 97,
+ risk: "low",
+ },
+ {
+ order: 95,
+ id: "fc6e179f-6d74-4517-9816-76d8b42b1ea9",
+ name: "Ethel Hauck",
+ city: "Kertzmannboro",
+ state: "Wisconsin",
+ age: 25,
+ risk: "high",
+ },
+ {
+ order: 96,
+ id: "b75376c7-f69c-4b47-a822-0746ee3f3cb9",
+ name: "Donnie Frami",
+ city: "Alameda",
+ state: "Louisiana",
+ age: 55,
+ risk: "medium",
+ },
+ {
+ order: 97,
+ id: "2b99b54b-8f74-4c4a-abe3-8c94043d95d7",
+ name: "Alberto Hyatt",
+ city: "Port Junior",
+ state: "Wisconsin",
+ age: 90,
+ risk: "low",
+ },
+ {
+ order: 98,
+ id: "d9c0ab78-e7aa-41b1-ab40-d5b2f8a046b0",
+ name: "James Grimes",
+ city: "Freddyshire",
+ state: "Wyoming",
+ age: 49,
+ risk: "high",
+ },
+ {
+ order: 99,
+ id: "2d9a8a19-5416-4913-bcc2-409868a50024",
+ name: "Carlos Bode",
+ city: "Trompfort",
+ state: "Connecticut",
+ age: 42,
+ risk: "medium",
+ },
+ {
+ order: 100,
+ id: "021b7bab-6093-456a-bdb9-e4d1cf8214ce",
+ name: "Krista Hoeger",
+ city: "Emelyland",
+ state: "Nebraska",
+ age: 98,
+ risk: "medium",
+ },
+ {
+ order: 101,
+ id: "120b3eaf-0ec7-44d7-a8fc-8a061c46dbc1",
+ name: "Dr. Leona Grant Sr.",
+ city: "Jamalborough",
+ state: "Nevada",
+ age: 27,
+ risk: "medium",
+ },
+ {
+ order: 102,
+ id: "bfad1a5a-d3be-4c36-8369-336b948e7bbd",
+ name: "Mandy Boehm",
+ city: "Orenboro",
+ state: "Virginia",
+ age: 92,
+ risk: "high",
+ },
+ {
+ order: 103,
+ id: "38aace7d-8996-45b4-8ee6-6b504ffb6c55",
+ name: "Samantha Strosin",
+ city: "MacGyverton",
+ state: "South Carolina",
+ age: 23,
+ risk: "low",
+ },
+ {
+ order: 104,
+ id: "7544c3ad-cb0f-4fee-ab76-d6563f6e5ce9",
+ name: "Veronica Daniel PhD",
+ city: "La Crosse",
+ state: "Kansas",
+ age: 44,
+ risk: "high",
+ },
+ {
+ order: 105,
+ id: "b790953f-3a82-4497-8952-9f98acd16b8f",
+ name: "Frederick Goyette",
+ city: "Port Emiechester",
+ state: "Rhode Island",
+ age: 98,
+ risk: "high",
+ },
+ {
+ order: 106,
+ id: "f16fc323-9d9c-4fec-b64e-98c527f85d59",
+ name: "Simon Bode",
+ city: "The Woodlands",
+ state: "Colorado",
+ age: 63,
+ risk: "low",
+ },
+ {
+ order: 107,
+ id: "7c3ad30f-19d1-4cac-8d59-eee121520030",
+ name: "Lula Nitzsche",
+ city: "Wellington",
+ state: "South Carolina",
+ age: 57,
+ risk: "medium",
+ },
+ {
+ order: 108,
+ id: "2f69ca95-2062-4d0f-9069-01858beca5ba",
+ name: "Ora Kirlin",
+ city: "Virgiefort",
+ state: "Tennessee",
+ age: 72,
+ risk: "low",
+ },
+ {
+ order: 109,
+ id: "508200c4-020d-4060-9a78-2b1886cca75f",
+ name: "Cornelius Schaefer",
+ city: "Dominicchester",
+ state: "Tennessee",
+ age: 38,
+ risk: "low",
+ },
+ {
+ order: 110,
+ id: "5b24a664-8a08-4bad-b52a-0e0f68f66ffe",
+ name: "Dr. Marcus Kling-Yost",
+ city: "Crooksside",
+ state: "North Dakota",
+ age: 18,
+ risk: "high",
+ },
+ {
+ order: 111,
+ id: "134f237b-bf40-4bfe-9e60-0d65b1fc1ec1",
+ name: "Sabrina Krajcik Sr.",
+ city: "Glovercester",
+ state: "New York",
+ age: 72,
+ risk: "high",
+ },
+ {
+ order: 112,
+ id: "a63fc694-108f-4fe3-a2aa-da09300e4e81",
+ name: "Harvey Raynor",
+ city: "Bettyland",
+ state: "Missouri",
+ age: 87,
+ risk: "medium",
+ },
+ {
+ order: 113,
+ id: "0376536e-5df7-4271-b9b7-fdad30359752",
+ name: "Doug Greenholt",
+ city: "North Celestinofort",
+ state: "Oklahoma",
+ age: 60,
+ risk: "medium",
+ },
+ {
+ order: 114,
+ id: "d4dce7c2-657d-486a-82e8-f6fc23f972c6",
+ name: "Tasha VonRueden",
+ city: "West Alfordberg",
+ state: "New Hampshire",
+ age: 20,
+ risk: "medium",
+ },
+ {
+ order: 115,
+ id: "e61960cd-bc35-41c7-b4d1-5e1485935f00",
+ name: "Mabel Wiza",
+ city: "Torphyburgh",
+ state: "Indiana",
+ age: 30,
+ risk: "low",
+ },
+ {
+ order: 116,
+ id: "d3f94cb1-3425-48fd-a714-fbf4c8bfa112",
+ name: "Glenn Murazik",
+ city: "Fort Jaimeside",
+ state: "Rhode Island",
+ age: 36,
+ risk: "medium",
+ },
+ {
+ order: 117,
+ id: "63556c18-c65a-4aaa-ba75-357702fc90a5",
+ name: "Nicole Schultz",
+ city: "Peabody",
+ state: "Missouri",
+ age: 25,
+ risk: "medium",
+ },
+ {
+ order: 118,
+ id: "a3a30238-64f7-4bf5-aa11-73956e2c4ccf",
+ name: "Tyler Moen V",
+ city: "South Cora",
+ state: "West Virginia",
+ age: 81,
+ risk: "low",
+ },
+ {
+ order: 119,
+ id: "f9641224-0dcf-45d3-b614-88ef5a33779d",
+ name: "Julius Haag",
+ city: "South Casimirboro",
+ state: "Alaska",
+ age: 28,
+ risk: "high",
+ },
+ {
+ order: 120,
+ id: "895a6558-dfd3-4b82-8ddf-63c55ca00500",
+ name: "Eunice Dach",
+ city: "Swaniawskiburgh",
+ state: "Montana",
+ age: 60,
+ risk: "high",
+ },
+ {
+ order: 121,
+ id: "1b9fd626-73ba-466a-b358-22a5237b7be5",
+ name: "Kristi Bernier",
+ city: "West Kenyattastead",
+ state: "Missouri",
+ age: 27,
+ risk: "high",
+ },
+ {
+ order: 122,
+ id: "3bf93192-0092-4969-8025-fe85e74a878b",
+ name: "Mrs. Meredith Schaefer",
+ city: "Port Norris",
+ state: "Pennsylvania",
+ age: 71,
+ risk: "high",
+ },
+ {
+ order: 123,
+ id: "ecf8e122-71e0-4d17-9cb7-26b15029c287",
+ name: "Sean Satterfield",
+ city: "Abernathyborough",
+ state: "Massachusetts",
+ age: 48,
+ risk: "low",
+ },
+ {
+ order: 124,
+ id: "79392c54-a933-4073-ad83-e9a20eb80cc3",
+ name: "Leona Effertz MD",
+ city: "Fort Crawfordchester",
+ state: "Hawaii",
+ age: 87,
+ risk: "medium",
+ },
+ {
+ order: 125,
+ id: "21af25ea-4fa9-46dd-8d0d-dd4c0ef82b9e",
+ name: "Andrew Schinner V",
+ city: "East Glennafort",
+ state: "California",
+ age: 84,
+ risk: "low",
+ },
+ {
+ order: 126,
+ id: "4b68f2eb-2952-4392-b96a-cc84b7cd798d",
+ name: "Silvia McCullough",
+ city: "West Seneca",
+ state: "Vermont",
+ age: 66,
+ risk: "high",
+ },
+ {
+ order: 127,
+ id: "b5acfe2f-fb43-4c35-9b6b-b7118eee61bf",
+ name: "Mrs. June Robel",
+ city: "Trantowbury",
+ state: "Iowa",
+ age: 75,
+ risk: "medium",
+ },
+ {
+ order: 128,
+ id: "4c6872ce-ae6b-40c7-bd81-396b26ddd35e",
+ name: "Katherine Kuphal",
+ city: "New Arnaldoside",
+ state: "Indiana",
+ age: 33,
+ risk: "medium",
+ },
+ {
+ order: 129,
+ id: "42244329-c344-4333-8ca7-980784df1099",
+ name: "Guadalupe Hayes",
+ city: "Collierside",
+ state: "Arizona",
+ age: 28,
+ risk: "low",
+ },
+ {
+ order: 130,
+ id: "37997d64-b87c-494e-9197-51c3851a961b",
+ name: "Brandi Beier",
+ city: "Rosenbaumshire",
+ state: "Montana",
+ age: 35,
+ risk: "high",
+ },
+ {
+ order: 131,
+ id: "20617263-4256-4053-bd33-80a9d108bde5",
+ name: "Ira Schiller",
+ city: "East Geraldine",
+ state: "Arkansas",
+ age: 87,
+ risk: "low",
+ },
+ {
+ order: 132,
+ id: "f4344a84-8a0e-48d4-8a4c-0fe24dd54249",
+ name: "Rene Torp",
+ city: "North Shaniacester",
+ state: "South Carolina",
+ age: 53,
+ risk: "medium",
+ },
+ {
+ order: 133,
+ id: "329c823b-dc98-4ad5-b866-8249b223c739",
+ name: "Beulah Prohaska",
+ city: "North Henri",
+ state: "Idaho",
+ age: 33,
+ risk: "medium",
+ },
+ {
+ order: 134,
+ id: "5a633cb6-77e0-495a-b9cb-2bd9e3c921fc",
+ name: "Raquel Glover",
+ city: "Rutherfordcester",
+ state: "Delaware",
+ age: 81,
+ risk: "low",
+ },
+ {
+ order: 135,
+ id: "f33377a1-ec5c-4b17-acd5-ff01f9ef549a",
+ name: "Lillie Bergnaum",
+ city: "Camden",
+ state: "Florida",
+ age: 23,
+ risk: "medium",
+ },
+ {
+ order: 136,
+ id: "2a0abf60-8c95-4334-8e65-41b9f9aea934",
+ name: "Blanche Rath",
+ city: "Cuyahoga Falls",
+ state: "Alaska",
+ age: 62,
+ risk: "low",
+ },
+ {
+ order: 137,
+ id: "ff8a2b6b-c0ac-4854-9d8e-468aa0c9c7df",
+ name: "Andres Sanford DVM",
+ city: "Hermanshire",
+ state: "Alabama",
+ age: 85,
+ risk: "high",
+ },
+ {
+ order: 138,
+ id: "78507d20-f4d2-4dc6-aeb8-688c5375c1eb",
+ name: "Jenny Cassin I",
+ city: "St. Louis",
+ state: "Florida",
+ age: 82,
+ risk: "low",
+ },
+ {
+ order: 139,
+ id: "8e5cc328-e048-4d24-9562-50834002c99a",
+ name: "Darla Corkery III",
+ city: "North Sigmund",
+ state: "Alaska",
+ age: 52,
+ risk: "low",
+ },
+ {
+ order: 140,
+ id: "6e74d62a-08a7-4060-ac78-723fef554369",
+ name: "Mr. Andres Fritsch",
+ city: "Mullershire",
+ state: "New Hampshire",
+ age: 74,
+ risk: "low",
+ },
+ {
+ order: 141,
+ id: "8d06d9ed-ee13-4c2d-9952-7d919ddba8f9",
+ name: "Cesar Effertz",
+ city: "South Kirstin",
+ state: "Oklahoma",
+ age: 63,
+ risk: "low",
+ },
+ {
+ order: 142,
+ id: "e8ed5718-2b8a-428f-92f5-98bd2aecefa2",
+ name: "Norman Von",
+ city: "Fort Dovieview",
+ state: "Oklahoma",
+ age: 93,
+ risk: "high",
+ },
+ {
+ order: 143,
+ id: "28323a88-867e-4989-a975-b2c15ae117d6",
+ name: "Gilbert Cremin",
+ city: "Dayanaborough",
+ state: "California",
+ age: 42,
+ risk: "medium",
+ },
+ {
+ order: 144,
+ id: "6def70a2-6d40-43cd-bb31-b936bdcb51e4",
+ name: "Nicolas Christiansen",
+ city: "Westborough",
+ state: "Indiana",
+ age: 29,
+ risk: "high",
+ },
+ {
+ order: 145,
+ id: "d84c1d33-c416-4cd9-a8e9-81e8a5f54c43",
+ name: "Cecelia Durgan-Stamm I",
+ city: "Visalia",
+ state: "New York",
+ age: 22,
+ risk: "medium",
+ },
+ {
+ order: 146,
+ id: "cc536114-3323-45bf-b980-aa952e441355",
+ name: "Chad Huels",
+ city: "Fountainebleau",
+ state: "Maryland",
+ age: 80,
+ risk: "high",
+ },
+ {
+ order: 147,
+ id: "a183e00e-e983-4ec4-a0d7-28fa3d839d98",
+ name: "Noel Bayer-Raynor",
+ city: "Naperville",
+ state: "Michigan",
+ age: 71,
+ risk: "low",
+ },
+ {
+ order: 148,
+ id: "2d02375d-ac1f-4d78-a90f-e9a99be143d9",
+ name: "Mr. Leo Feil",
+ city: "Lake Marshallboro",
+ state: "New Jersey",
+ age: 46,
+ risk: "low",
+ },
+ {
+ order: 149,
+ id: "46e39993-14d9-45f9-a58a-51ed44d1f2cf",
+ name: "Marco Rath",
+ city: "Lake Damarisview",
+ state: "Alabama",
+ age: 25,
+ risk: "medium",
+ },
+ {
+ order: 150,
+ id: "eaac5ba1-50b6-46d7-86bd-78a5e4e93891",
+ name: "Terence Vandervort",
+ city: "Philadelphia",
+ state: "Idaho",
+ age: 30,
+ risk: "low",
+ },
+ {
+ order: 151,
+ id: "82c596f1-6f8d-464b-8746-37cab5b3a955",
+ name: "Steven Gibson III",
+ city: "Port Lessieburgh",
+ state: "Michigan",
+ age: 61,
+ risk: "medium",
+ },
+ {
+ order: 152,
+ id: "6ee0e5d9-d82c-4e29-8e49-c879c422b572",
+ name: "Jenny Kutch",
+ city: "Irwinboro",
+ state: "Louisiana",
+ age: 78,
+ risk: "high",
+ },
+ {
+ order: 153,
+ id: "e799e19f-e704-4e08-929f-4d35a1e3c347",
+ name: "Albert Kihn",
+ city: "South Marquis",
+ state: "New Hampshire",
+ age: 33,
+ risk: "medium",
+ },
+ {
+ order: 154,
+ id: "608d06fc-6952-44bd-809b-db8b9398cedc",
+ name: "Christine Satterfield",
+ city: "Angelicatown",
+ state: "Oregon",
+ age: 25,
+ risk: "high",
+ },
+ {
+ order: 155,
+ id: "ad1f7bb5-894e-4fa5-b1ff-d66c14960ef1",
+ name: "Colleen Bartoletti",
+ city: "North Alverta",
+ state: "Indiana",
+ age: 55,
+ risk: "low",
+ },
+ {
+ order: 156,
+ id: "2999d3dc-22a9-4830-9c3f-5ace33b3a299",
+ name: "Dr. Jody Fisher",
+ city: "Harveyside",
+ state: "Arkansas",
+ age: 32,
+ risk: "high",
+ },
+ {
+ order: 157,
+ id: "58cf6f57-e4c6-4d1f-a172-abddbfeacef1",
+ name: "Miss Marta D'Amore",
+ city: "Catharineview",
+ state: "Alaska",
+ age: 21,
+ risk: "high",
+ },
+ {
+ order: 158,
+ id: "1f81b31a-bba7-400a-ba92-71f489c93fd5",
+ name: "Darrell West",
+ city: "Marianoshire",
+ state: "New Hampshire",
+ age: 30,
+ risk: "low",
+ },
+ {
+ order: 159,
+ id: "850f112e-aa45-486b-bb74-c5d7fb4fe258",
+ name: "Myron Schmitt",
+ city: "Kalebbury",
+ state: "Connecticut",
+ age: 46,
+ risk: "low",
+ },
+ {
+ order: 160,
+ id: "0ed4118c-223e-4330-b0b2-950884c52962",
+ name: "Kay Ferry III",
+ city: "Shayleeport",
+ state: "Kentucky",
+ age: 53,
+ risk: "medium",
+ },
+ {
+ order: 161,
+ id: "62ab7162-c488-47e8-a8e9-549f343e70c5",
+ name: "Geoffrey Mayert",
+ city: "Noblesville",
+ state: "South Carolina",
+ age: 46,
+ risk: "high",
+ },
+ {
+ order: 162,
+ id: "d35452b3-43ca-48e6-bcdd-61e8066ea2ac",
+ name: "Daisy Blanda",
+ city: "Paulton",
+ state: "Kentucky",
+ age: 87,
+ risk: "high",
+ },
+ {
+ order: 163,
+ id: "f66a3afe-92c3-43c7-84db-9430ae703822",
+ name: "Lindsay Stracke",
+ city: "Schulistside",
+ state: "New York",
+ age: 42,
+ risk: "medium",
+ },
+ {
+ order: 164,
+ id: "14d84b7e-c61d-4fe9-a88a-758d19b065b4",
+ name: "Derrick Schulist",
+ city: "Rosinatown",
+ state: "Nevada",
+ age: 47,
+ risk: "low",
+ },
+ {
+ order: 165,
+ id: "19970a29-2075-42c0-a3ad-ed018cac00d5",
+ name: "Latoya Kub",
+ city: "South Rollinport",
+ state: "South Dakota",
+ age: 47,
+ risk: "low",
+ },
+ {
+ order: 166,
+ id: "17a195ad-cfc5-4801-97dc-8d0deceba057",
+ name: "Mr. Mike Bechtelar",
+ city: "East Jeremiemouth",
+ state: "New Mexico",
+ age: 59,
+ risk: "low",
+ },
+ {
+ order: 167,
+ id: "2390b432-c739-4fcd-98a7-de3d18581c17",
+ name: "Orville Rowe",
+ city: "Hendersonville",
+ state: "Connecticut",
+ age: 57,
+ risk: "high",
+ },
+ {
+ order: 168,
+ id: "eaa0b563-6bb6-436a-94a6-352c292fd24a",
+ name: "Camille Kerluke",
+ city: "Port Lyda",
+ state: "Florida",
+ age: 38,
+ risk: "high",
+ },
+ {
+ order: 169,
+ id: "f6e1cbfa-5b39-4506-b03a-ce5275dff60d",
+ name: "Miss Antonia Rodriguez",
+ city: "Amarillo",
+ state: "Wyoming",
+ age: 72,
+ risk: "high",
+ },
+ {
+ order: 170,
+ id: "9f76ac34-5d6d-402c-b572-fec675d23c74",
+ name: "Willard Turner",
+ city: "New Thelma",
+ state: "New Jersey",
+ age: 53,
+ risk: "low",
+ },
+ {
+ order: 171,
+ id: "1e6b930a-a69e-40eb-90c6-37578bdb66a8",
+ name: "Annette Berge",
+ city: "Carson",
+ state: "Wisconsin",
+ age: 57,
+ risk: "low",
+ },
+ {
+ order: 172,
+ id: "3fa71029-8ae2-4db4-b0c2-ebb51c22d368",
+ name: "Katrina Roberts I",
+ city: "Fort Odie",
+ state: "Arkansas",
+ age: 76,
+ risk: "high",
+ },
+ {
+ order: 173,
+ id: "12d7fa49-bae2-4cc8-b126-9d653be473ef",
+ name: "Sylvia Rau",
+ city: "Oxnard",
+ state: "Hawaii",
+ age: 25,
+ risk: "high",
+ },
+ {
+ order: 174,
+ id: "2dbb95ed-c460-4594-a1a9-1c06932e2e6f",
+ name: "Gene Schulist",
+ city: "Fort Libbieboro",
+ state: "Oklahoma",
+ age: 51,
+ risk: "low",
+ },
+ {
+ order: 175,
+ id: "800e2b2b-cd4f-4007-99ce-a14c325b6bd0",
+ name: "Richard Kassulke",
+ city: "Riverton",
+ state: "Oklahoma",
+ age: 84,
+ risk: "high",
+ },
+ {
+ order: 176,
+ id: "26e61243-d236-4a14-a1ba-a03d7ce5a530",
+ name: "Yvette Orn",
+ city: "Royal Oak",
+ state: "Iowa",
+ age: 20,
+ risk: "high",
+ },
+ {
+ order: 177,
+ id: "7897807e-04ff-469a-a7ff-a5ff40ccdf7c",
+ name: "Sherri Hand",
+ city: "New Zoila",
+ state: "Nevada",
+ age: 34,
+ risk: "medium",
+ },
+ {
+ order: 178,
+ id: "785eb865-78bf-4676-b087-9d2c5622d8f4",
+ name: "Tonya Simonis",
+ city: "Jeremiefort",
+ state: "Texas",
+ age: 58,
+ risk: "low",
+ },
+ {
+ order: 179,
+ id: "c0f0ce78-d187-43cf-9406-782843972ef7",
+ name: "Sandy Schulist-Rowe",
+ city: "Heaneyville",
+ state: "Florida",
+ age: 78,
+ risk: "high",
+ },
+ {
+ order: 180,
+ id: "b8111699-6d4d-402b-867b-e4ed9ee4a0d1",
+ name: "Michele Crona",
+ city: "South Nelle",
+ state: "South Carolina",
+ age: 46,
+ risk: "low",
+ },
+ {
+ order: 181,
+ id: "422977f9-3c22-4ef8-8a6a-a4f09245bc31",
+ name: "Lucy Welch",
+ city: "West Shannon",
+ state: "Tennessee",
+ age: 47,
+ risk: "medium",
+ },
+ {
+ order: 182,
+ id: "5de4ada2-f04f-4487-9443-b1c015db5508",
+ name: "Thelma Toy",
+ city: "Boca Raton",
+ state: "Connecticut",
+ age: 31,
+ risk: "high",
+ },
+ {
+ order: 183,
+ id: "afc75679-ae2d-4ef3-a7a1-9f2fc6228c78",
+ name: "Amelia Vandervort-Howe",
+ city: "Miami Beach",
+ state: "Massachusetts",
+ age: 58,
+ risk: "high",
+ },
+ {
+ order: 184,
+ id: "23e37a07-0a7b-476a-a476-829a1eaff962",
+ name: "Loretta Ernser V",
+ city: "Euless",
+ state: "New York",
+ age: 35,
+ risk: "low",
+ },
+ {
+ order: 185,
+ id: "7086ae68-6d7d-45c0-aa4f-7d10d946283a",
+ name: "Phyllis Grimes",
+ city: "New Bretcester",
+ state: "California",
+ age: 43,
+ risk: "low",
+ },
+ {
+ order: 186,
+ id: "582429f2-dfdf-40b3-a164-ee5f665391e8",
+ name: "Eloise Von",
+ city: "Violacester",
+ state: "Alabama",
+ age: 31,
+ risk: "medium",
+ },
+ {
+ order: 187,
+ id: "969de9bc-47ef-47cb-b716-a74c0abbf05c",
+ name: "Lula Schuster",
+ city: "Hazleside",
+ state: "Arkansas",
+ age: 73,
+ risk: "high",
+ },
+ {
+ order: 188,
+ id: "55b28840-d93c-43b4-b552-f4256dfdf389",
+ name: "Dr. Bertha Volkman",
+ city: "New Bethelhaven",
+ state: "Alabama",
+ age: 38,
+ risk: "medium",
+ },
+ {
+ order: 189,
+ id: "cb1d5f44-0d3e-4bdd-8ad4-ce43e459870b",
+ name: "Marco Collier",
+ city: "New Marlon",
+ state: "Iowa",
+ age: 24,
+ risk: "high",
+ },
+ {
+ order: 190,
+ id: "a0a3596e-a259-4c9e-bce0-b163a1156363",
+ name: "Alex Cormier III",
+ city: "Kirstenfort",
+ state: "Pennsylvania",
+ age: 98,
+ risk: "low",
+ },
+ {
+ order: 191,
+ id: "9f0c5912-be82-48e0-9361-434b4d2afdfd",
+ name: "Kay Thompson",
+ city: "Visalia",
+ state: "Kentucky",
+ age: 91,
+ risk: "medium",
+ },
+ {
+ order: 192,
+ id: "370c711c-ff45-41d3-a911-a176c5e6ec57",
+ name: "Mae Daugherty",
+ city: "Fort Wanda",
+ state: "Kansas",
+ age: 68,
+ risk: "high",
+ },
+ {
+ order: 193,
+ id: "6e274204-ee56-4c42-b058-d90c2ca4d2f9",
+ name: "Nancy Adams",
+ city: "Edgarport",
+ state: "Louisiana",
+ age: 90,
+ risk: "medium",
+ },
+ {
+ order: 194,
+ id: "94c130c1-2926-42ff-bae0-65f5daf0e04e",
+ name: "Tara Douglas",
+ city: "South Reina",
+ state: "Mississippi",
+ age: 78,
+ risk: "low",
+ },
+ {
+ order: 195,
+ id: "7d817f75-dea8-4fe4-b255-b09dc7adeea0",
+ name: "Miss Patricia Bradtke MD",
+ city: "Port Eusebiofort",
+ state: "Montana",
+ age: 20,
+ risk: "low",
+ },
+ {
+ order: 196,
+ id: "61e3358f-55bc-4aa6-80f9-fd418a428539",
+ name: "Robert Schowalter",
+ city: "Lake Filiberto",
+ state: "North Dakota",
+ age: 44,
+ risk: "high",
+ },
+ {
+ order: 197,
+ id: "4115db11-7c19-43cd-870b-143fc26cd3d8",
+ name: "Tyler Cole",
+ city: "West Dana",
+ state: "Connecticut",
+ age: 86,
+ risk: "low",
+ },
+ {
+ order: 198,
+ id: "946dfdaa-6fc9-4cf9-b1fe-b9d4ef32dd4f",
+ name: "Dora Muller",
+ city: "Irvine",
+ state: "Kansas",
+ age: 25,
+ risk: "high",
+ },
+ {
+ order: 199,
+ id: "34fc92c9-ac5a-4a8d-965d-412d47f640b8",
+ name: "Paulette Reichert",
+ city: "Dublin",
+ state: "Hawaii",
+ age: 45,
+ risk: "medium",
+ },
+];
diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/Card/Card.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/Card/Card.stories.tsx
index 7822585c91..2f64709ce5 100644
--- a/packages/odyssey-storybook/src/components/odyssey-mui/Card/Card.stories.tsx
+++ b/packages/odyssey-storybook/src/components/odyssey-mui/Card/Card.stories.tsx
@@ -148,7 +148,9 @@ export const Clickable: StoryObj = {
return (
}
onClick={onClick}
/>
@@ -165,7 +167,13 @@ export const ClickableWithoutImage: StoryObj = {
return (
-
+
);
},
@@ -174,7 +182,20 @@ export const ClickableWithoutImage: StoryObj = {
export const ButtonWithoutImage: StoryObj = {
render: ({ ...props }) => (
-
+
+
+
+
+ >
+ }
+ button={}
+ />
),
};
diff --git a/packages/odyssey-storybook/src/components/odyssey-mui/DataTable/DataTable.stories.tsx b/packages/odyssey-storybook/src/components/odyssey-mui/DataTable/DataTable.stories.tsx
index 22d8724e94..bfcf9b6ac5 100644
--- a/packages/odyssey-storybook/src/components/odyssey-mui/DataTable/DataTable.stories.tsx
+++ b/packages/odyssey-storybook/src/components/odyssey-mui/DataTable/DataTable.stories.tsx
@@ -18,7 +18,7 @@ import {
import {
Box,
Button,
- DataTable,
+ // DataTable,
EmptyState,
DataTableGetDataType,
DataTableOnReorderRowsType,
@@ -26,8 +26,9 @@ import {
DataTableRenderDetailPanelType,
DataTableRowSelectionState,
MenuItem,
- densityValues,
+ // densityValues,
} from "@okta/odyssey-react-mui";
+import { DataTable, densityValues } from "@okta/odyssey-react-mui/labs";
import { MuiThemeDecorator } from "../../../../.storybook/components";
import {
Planet,