Skip to content

Commit

Permalink
TableView enhancements
Browse files Browse the repository at this point in the history
  • Loading branch information
imanjra committed Oct 17, 2024
1 parent 7a7dfdd commit efac7b6
Show file tree
Hide file tree
Showing 3 changed files with 303 additions and 21 deletions.
108 changes: 108 additions & 0 deletions app/packages/core/src/plugins/SchemaIO/components/ActionsMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { MuiIconFont } from "@fiftyone/components";
import { MoreVert } from "@mui/icons-material";
import {
Box,
Button,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Stack,
} from "@mui/material";
import React, { useCallback } from "react";

const DEFAULT_MAX_INLINE = 1;

export default function ActionsMenu(props: ActionsPropsType) {
const { actions, maxInline = DEFAULT_MAX_INLINE } = props;

if (actions.length === maxInline) {
return (
<Stack direction="row" spacing={0.5} justifyContent="flex-end">
{actions.map((action) => (
<Action {...action} key={action.name} mode="inline" />
))}
</Stack>
);
}

return <ActionsOverflowMenu actions={actions} />;
}

function ActionsOverflowMenu(props: ActionsPropsType) {
const { actions } = props;
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef(null);

const handleClose = useCallback(() => {
setOpen(false);
}, []);

return (
<Box>
<IconButton
onClick={() => {
setOpen(!open);
}}
ref={anchorRef}
>
<MoreVert />
</IconButton>
<Menu open={open} onClose={handleClose} anchorEl={anchorRef.current}>
{actions.map((action) => {
const { name, onClick } = action;
return (
<Action
key={name}
{...action}
mode="menu"
onClick={(action, e) => {
handleClose();
onClick?.(action, e);
}}
/>
);
})}
</Menu>
</Box>
);
}

function Action(props: ActionPropsType) {
const { label, name, onClick, icon, variant, mode } = props;

const Icon = icon ? <MuiIconFont name={icon} /> : null;

const handleClick = useCallback(
(e: React.MouseEvent) => {
onClick?.(props, e);
},
[onClick, props]
);

return mode === "inline" ? (
<Button variant={variant} startIcon={Icon} onClick={handleClick}>
{label}
</Button>
) : (
<MenuItem onClick={handleClick}>
{Icon && <ListItemIcon>{Icon}</ListItemIcon>}
<ListItemText>{label || name}</ListItemText>
</MenuItem>
);
}

type ActionsPropsType = {
actions: Array<ActionPropsType>;
maxInline?: number;
};

type ActionPropsType = {
name: string;
label: string;
onClick: (action: ActionPropsType, e: React.MouseEvent) => void;
icon: string;
variant: string;
mode: "inline" | "menu";
};
184 changes: 163 additions & 21 deletions app/packages/core/src/plugins/SchemaIO/components/TableView.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { scrollable } from "@fiftyone/components";
import { usePanelEvent } from "@fiftyone/operators";
import { usePanelId } from "@fiftyone/spaces";
import {
Box,
Paper,
Expand All @@ -8,23 +11,49 @@ import {
TableHead,
TableRow,
} from "@mui/material";
import React from "react";
import { isPlainObject } from "lodash";
import React, { useCallback } from "react";
import { HeaderView } from ".";
import EmptyState from "./EmptyState";
import { getComponentProps } from "../utils";
import { ViewPropsType } from "../utils/types";
import ActionsMenu from "./ActionsMenu";
import EmptyState from "./EmptyState";

export default function TableView(props) {
const { schema, data } = props;
const { view = {}, default: defaultValue } = schema;
const { columns } = view;

const table = Array.isArray(data)
? data
: Array.isArray(defaultValue)
? defaultValue
: [];
export default function TableView(props: ViewPropsType) {
const { path, schema } = props;
const { view = {} } = schema;
const {
columns,
row_actions = [],
on_click_cell,
on_click_row,
on_click_column,
actions_label,
} = view;
const { rows, selectedCells, selectedRows, selectedColumns } =
getTableData(props);
const dataMissing = rows.length === 0;
const hasRowActions = row_actions.length > 0;
const panelId = usePanelId();
const handleClick = usePanelEvent();

const dataMissing = table.length === 0;
const getRowActions = useCallback((row) => {
const computedRowActions = [] as any;
for (const action of row_actions) {
if (action.rows?.[row] !== false) {
computedRowActions.push({
...action,
onClick: (action, e) => {
handleClick(panelId, {
operator: action.on_click,
params: { path, event: action.name, row },
});
},
});
}
}
return computedRowActions;
}, []);

return (
<Box {...getComponentProps(props, "container")}>
Expand All @@ -33,6 +62,7 @@ export default function TableView(props) {
{!dataMissing && (
<TableContainer
component={Paper}
className={scrollable}
{...getComponentProps(props, "tableContainer")}
>
<Table sx={{ minWidth: 650 }} {...getComponentProps(props, "table")}>
Expand All @@ -47,23 +77,82 @@ export default function TableView(props) {
{label}
</TableCell>
))}
{hasRowActions && (
<TableCell
{...getComponentProps(props, "tableHeadCell", {
sx: {
textAlign: "right",
fontWeight: 600,
fontSize: "1rem",
},
})}
>
{actions_label || "Actions"}
</TableCell>
)}
</TableRow>
</TableHead>
<TableBody {...getComponentProps(props, "tableBody")}>
{table.map((item) => (
{rows.map((item, rowIndex) => (
<TableRow
key={item.id}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
{...getComponentProps(props, "tableBodyRow")}
>
{columns.map(({ key }) => (
<TableCell
key={key}
{...getComponentProps(props, "tableBodyCell")}
>
{item[key]}
{columns.map(({ key }, columnIndex) => {
const coordinate = [rowIndex, columnIndex].join(",");
const isSelected =
selectedCells.has(coordinate) ||
selectedRows.has(rowIndex) ||
selectedColumns.has(columnIndex);
return (
<TableCell
key={key}
sx={{ background: isSelected ? "green" : "unset" }}
onClick={() => {
if (on_click_cell) {
handleClick(panelId, {
operator: on_click_cell,
params: {
row: rowIndex,
column: columnIndex,
path,
event: "on_click_cell",
},
});
}
if (on_click_row) {
handleClick(panelId, {
operator: on_click_row,
params: {
row: rowIndex,
path,
event: "on_click_row",
},
});
}
if (on_click_column) {
handleClick(panelId, {
operator: on_click_column,
params: {
column: columnIndex,
path,
event: "on_click_column",
},
});
}
}}
{...getComponentProps(props, "tableBodyCell")}
>
{formatCellValue(item[key], props)}
</TableCell>
);
})}
{hasRowActions && (
<TableCell align="right">
<ActionsMenu actions={getRowActions(rowIndex)} />
</TableCell>
))}
)}
</TableRow>
))}
</TableBody>
Expand All @@ -73,3 +162,56 @@ export default function TableView(props) {
</Box>
);
}

function getTableData(props) {
const { schema, data } = props;
const defaultValue = schema?.default;

if (isAdvancedData(data)) {
return parseAdvancedData(data);
}
if (isAdvancedData(defaultValue)) {
return parseAdvancedData(defaultValue);
}
return {
rows: Array.isArray(data)
? data
: Array.isArray(defaultValue)
? defaultValue
: [],
};
}

function isAdvancedData(data) {
return (
isPlainObject(data) &&
Array.isArray(data?.rows) &&
Array.isArray(data?.columns)
);
}

function parseAdvancedData(data) {
const rows = data.rows.map((row) => {
return data.columns.reduce((cells, column, cellIndex) => {
cells[column] = row[cellIndex];
return cells;
}, {});
});
const selectedCellsRaw = data?.selectedCells || data?.selected_cells || [];
const selectedRowsRaw = data?.selectedRows || data?.selected_rows || [];
const selectedColumnsRaw =
data?.selectedColumns || data?.selected_columns || [];
const selectedCells = new Set(selectedCellsRaw.map((cell) => cell.join(",")));
const selectedRows = new Set(selectedRowsRaw);
const selectedColumns = new Set(selectedColumnsRaw);
return { rows, selectedCells, selectedRows, selectedColumns };
}

function formatCellValue(value: string, props: ViewPropsType) {
const round = props?.schema?.view?.round;
const valueAsFloat = parseFloat(value);
if (!Number.isNaN(valueAsFloat) && typeof round === "number") {
return valueAsFloat.toFixed(round);
}
return value;
}
32 changes: 32 additions & 0 deletions fiftyone/operators/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1618,16 +1618,39 @@ def to_json(self):
return {**super().to_json(), "key": self.key}


class Action(View):
"""An action (currently supported only in a :class:`TableView`).
Args:
name: the name of the action
label (None): the label of the action
icon (None): the icon of the action
on_click: the operator to execute when the action is clicked
"""

def __init__(self, **kwargs):
super().__init__(**kwargs)

def clone(self):
clone = Action(**self._kwargs)
return clone

def to_json(self):
return {**super().to_json()}


class TableView(View):
"""Displays a table.
Args:
columns (None): a list of :class:`Column` objects to display
row_actions (None): a list of :class:`Action` objects to display
"""

def __init__(self, **kwargs):
super().__init__(**kwargs)
self.columns = kwargs.get("columns", [])
self.row_actions = kwargs.get("row_actions", [])

def keys(self):
return [column.key for column in self.columns]
Expand All @@ -1637,15 +1660,24 @@ def add_column(self, key, **kwargs):
self.columns.append(column)
return column

def add_row_action(self, name, on_click, label=None, icon=None, **kwargs):
row_action = Action(
name=name, on_click=on_click, label=label, icon=icon, **kwargs
)
self.row_actions.append(row_action)
return row_action

def clone(self):
clone = super().clone()
clone.columns = [column.clone() for column in self.columns]
clone.row_actions = [action.clone() for action in self.row_actions]
return clone

def to_json(self):
return {
**super().to_json(),
"columns": [column.to_json() for column in self.columns],
"row_actions": [action.to_json() for action in self.row_actions],
}


Expand Down

0 comments on commit efac7b6

Please sign in to comment.