Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DashboardView #4557

Merged
merged 15 commits into from
Aug 2, 2024
2 changes: 2 additions & 0 deletions app/packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"react-draggable": "^4.4.5",
"react-error-boundary": "^3.1.4",
"react-file-drop": "^3.1.6",
"react-grid-layout": "^1.4.4",
"react-hotkeys": "^2.0.0",
"react-input-autosize": "^3.0.0",
"react-is": "^17.0.1",
Expand All @@ -60,6 +61,7 @@
"@types/react": "^18.0.9",
"@types/react-color": "^3.0.6",
"@types/react-dom": "^18.0.3",
"@types/react-grid-layout": "^1.3.5",
"@types/react-relay": "^14.1.0",
"@types/react-router": "^5.1.18",
"@types/relay-compiler": "^8.0.2",
Expand Down
284 changes: 284 additions & 0 deletions app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
import { useTheme } from "@fiftyone/components";
import usePanelEvent from "@fiftyone/operators/src/usePanelEvent";
import { usePanelId } from "@fiftyone/spaces";
import CloseIcon from "@mui/icons-material/Close";
import {
Box,
BoxProps,
Grid,
IconButton,
Paper,
styled,
Typography,
} from "@mui/material";
import React, { forwardRef, useCallback, useState } from "react";
import GridLayout from "react-grid-layout";
import "react-grid-layout/css/styles.css";
import "react-resizable/css/styles.css";
import { Button } from ".";
import { getComponentProps, getPath, getProps } from "../utils";
import { ObjectSchemaType, ViewPropsType } from "../utils/types";
import DynamicIO from "./DynamicIO";
import { c } from "vite/dist/node/types.d-aGj9QkWt";

const AddItemCTA = ({ onAdd }) => {
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
height: "100%",
width: "100%",
}}
>
<Paper sx={{ padding: 2 }}>
<Typography variant="h4" component="h1" gutterBottom>
Add an Item to Your Dashboard
</Typography>
<Button variant="contained" onClick={onAdd}>
Add Item
</Button>
</Paper>
</Box>
);
};
ritch marked this conversation as resolved.
Show resolved Hide resolved
const AddItemButton = ({ onAddItem }) => {
return (
<Grid container spacing={2} style={{ position: "fixed", bottom: 0 }}>
<Grid item xs={12}>
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100px"
width="100%"
>
<Button variant="contained" size="large" onClick={onAddItem}>
Add New Item
</Button>
</Box>
</Grid>
</Grid>
);
Comment on lines +47 to +64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider memoizing the AddItemButton component.

To improve performance, consider memoizing the AddItemButton component using React.memo.

- const AddItemButton = ({ onAddItem }) => {
+ const AddItemButton = React.memo(({ onAddItem }) => {
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const AddItemButton = ({ onAddItem }) => {
return (
<Grid container spacing={2} style={{ position: "fixed", bottom: 0 }}>
<Grid item xs={12}>
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100px"
width="100%"
>
<Button variant="contained" size="large" onClick={onAddItem}>
Add New Item
</Button>
</Box>
</Grid>
</Grid>
);
const AddItemButton = React.memo(({ onAddItem }) => {
return (
<Grid container spacing={2} style={{ position: "fixed", bottom: 0 }}>
<Grid item xs={12}>
<Box
display="flex"
justifyContent="center"
alignItems="center"
height="100px"
width="100%"
>
<Button variant="contained" size="large" onClick={onAddItem}>
Add New Item
</Button>
</Box>
</Grid>
</Grid>
);

};

export default function DashboardView(props: ViewPropsType) {
const { schema, path, data, layout } = props;
const { properties } = schema as ObjectSchemaType;
const propertiesAsArray = [];
const allow_addition = schema.view.allow_addition;
const allow_deletion = schema.view.allow_deletion;

for (const property in properties) {
propertiesAsArray.push({ id: property, ...properties[property] });
}
const panelId = usePanelId();
const triggerPanelEvent = usePanelEvent();

const onCloseItem = useCallback(
({ id, path }) => {
if (schema.view.on_remove_item) {
triggerPanelEvent(panelId, {
operator: schema.view.on_remove_item,
params: { id, path },
});
}
},
[panelId, props, schema.view.on_remove_item, triggerPanelEvent]
);
const onAddItem = useCallback(() => {
if (schema.view.on_add_item) {
triggerPanelEvent(panelId, {
operator: schema.view.on_add_item,
});
}
}, [panelId, props, schema.view.on_add_item, triggerPanelEvent]);
const handleLayoutChange = useCallback(
(layout: any) => {
if (schema.view.on_layout_change) {
triggerPanelEvent(panelId, {
operator: schema.view.on_layout_change,
params: { layout },
});
}
},
[panelId, props, schema.view.on_layout_change, triggerPanelEvent]
);
const [isDragging, setIsDragging] = useState(false);
const theme = useTheme();

const baseGridProps: BoxProps = {};
const MIN_ITEM_WIDTH = 400;
const MIN_ITEM_HEIGHT = 300; // Setting minimum height for items
const GRID_WIDTH = layout?.width; // Set based on your container's width
const GRID_HEIGHT = layout?.height - 180; // Set based on your container's height - TODO remove button height hardcoded
Comment on lines +112 to +116
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid hardcoding layout values.

Avoid hardcoding layout values like GRID_HEIGHT. Consider making these values configurable.

- const GRID_HEIGHT = layout?.height - 180; // Set based on your container's height - TODO remove button height hardcoded
+ const GRID_HEIGHT = layout?.height - BUTTON_HEIGHT; // Set based on your container's height
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const baseGridProps: BoxProps = {};
const MIN_ITEM_WIDTH = 400;
const MIN_ITEM_HEIGHT = 300; // Setting minimum height for items
const GRID_WIDTH = layout?.width; // Set based on your container's width
const GRID_HEIGHT = layout?.height - 180; // Set based on your container's height - TODO remove button height hardcoded
const baseGridProps: BoxProps = {};
const MIN_ITEM_WIDTH = 400;
const MIN_ITEM_HEIGHT = 300; // Setting minimum height for items
const GRID_WIDTH = layout?.width; // Set based on your container's width
const GRID_HEIGHT = layout?.height - BUTTON_HEIGHT; // Set based on your container's height

const COLS = Math.floor(GRID_WIDTH / MIN_ITEM_WIDTH);
const ROWS = Math.ceil(propertiesAsArray.length / COLS);

const viewLayout = schema.view.layout;
const defaultLayout = propertiesAsArray.map((property, index) => {
return {
i: property.id,
x: index % COLS, // Correctly position items in the grid
y: Math.floor(index / COLS), // Correctly position items in the grid
w: 1,
h: 1, // Each item takes one row
minW: 1, // Minimum width in grid units
minH: Math.ceil(MIN_ITEM_HEIGHT / (GRID_HEIGHT / ROWS)), // Minimum height in grid units
};
});
const gridLayout = viewLayout || defaultLayout;

const DragHandle = styled(Box)(({ theme }) => ({
ritch marked this conversation as resolved.
Show resolved Hide resolved
cursor: "move",
backgroundColor: theme.palette.background.default,
color: theme.palette.text.secondary,
padding: theme.spacing(0.25),
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}));
const [showGrid, setShowGrid] = useState(false);
const toggleGrid = useCallback(() => setShowGrid(!showGrid), [showGrid]);

if (!propertiesAsArray.length) {
if (!allow_addition) {
return null;
}
return <AddItemCTA onAdd={onAddItem} />;
}
const finalLayout = [
...gridLayout,
{ i: "add-item", x: 0, y: ROWS, w: COLS, h: 1, static: true },
];

return (
<Box>
{!showGrid && <Button onClick={toggleGrid}>Toggle Grid</Button>}
{showGrid && (
<GridLayout
onLayoutChange={handleLayoutChange}
layout={finalLayout}
cols={COLS}
rowHeight={GRID_HEIGHT / ROWS} // Dynamic row height
width={GRID_WIDTH}
onDragStart={() => setIsDragging(true)}
onDragStop={() => setIsDragging(false)}
resizeHandles={["e", "w", "n", "s"]}
isDraggable={!isDragging}
isResizable={!isDragging} // Allow resizing
draggableHandle=".drag-handle"
resizeHandle={(axis, ref) => {
return <DashboardItemResizeHandle axis={axis} ref={ref} />;
}}
>
{propertiesAsArray.map((property) => {
const { id } = property;
const itemPath = getPath(path, id);
const baseItemProps: BoxProps = {
sx: { padding: 0.25, position: "relative" },
key: id,
};

return (
<Box
key={id}
{...getProps(
{ ...props, schema: property },
"item",
baseItemProps
)}
>
<DragHandle className="drag-handle">
<Typography>{property.title || id}</Typography>
{allow_deletion && (
<IconButton
size="small"
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
onCloseItem({ id, path: getPath(path, id) });
}}
sx={{ color: theme.text.secondary }}
>
<CloseIcon />
</IconButton>
)}
</DragHandle>
<Box sx={{ height: "calc(100% - 35px)", overflow: "auto" }}>
<DynamicIO
{...props}
schema={property}
path={itemPath}
data={data?.[id]}
parentSchema={schema}
relativePath={id}
/>
</Box>
</Box>
);
})}
</GridLayout>
)}
{allow_addition && <AddItemButton key="add-item" onAddItem={onAddItem} />}
</Box>
);
}

const DashboardItemResizeHandle = forwardRef((props, ref) => {
const theme = useTheme();
const { axis } = props;

const axisSx = AXIS_SX[axis] || {};

return (
<Typography
ref={ref}
sx={{
...axisSx,
position: "absolute",
borderColor: theme.neutral.plainColor,
opacity: 0,
transition: "opacity 0.25s",
"&:hover": {
opacity: 1,
},
}}
aria-label={`Resize ${axis}`}
{...props}
/>
);
});
ritch marked this conversation as resolved.
Show resolved Hide resolved

const AXIS_SX = {
e: {
height: "100%",
right: 0,
top: 0,
borderRight: "2px solid",
cursor: "e-resize",
},
w: {
height: "100%",
left: 0,
top: 0,
borderLeft: "2px solid",
cursor: "w-resize",
},
s: {
width: "100%",
bottom: 0,
left: 0,
borderBottom: "2px solid",
cursor: "s-resize",
},
n: {
width: "100%",
top: 0,
left: 0,
borderTop: "2px solid",
cursor: "n-resize",
},
};
Comment on lines +255 to +284
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider moving AXIS_SX outside of the file.

To improve maintainability, consider moving the AXIS_SX constant to a separate file.

- const AXIS_SX = {
+ export const AXIS_SX = {

Committable suggestion was skipped due to low confidence.

31 changes: 17 additions & 14 deletions app/packages/core/src/plugins/SchemaIO/components/PlotlyView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ export default function PlotlyView(props) {
let range = [0, 0];
const triggerPanelEvent = usePanelEvent();
const handleEvent = (event?: string) => (e) => {
// TODO: add more interesting/useful event data
const data = EventDataMappers[event]?.(e) || {};
const x_data_source = view.x_data_source;
let xValue = null;
let yValue = null;
if (event === "onClick") {
Expand All @@ -37,9 +35,12 @@ export default function PlotlyView(props) {
range = [xValue - xBinsSize / 2, xValue + xBinsSize / 2];
} else if (type === "scatter") {
selected.push(p.pointIndex);
xValue = p.x;
yValue = p.y;
} else if (type === "bar") {
xValue = p.x;
yValue = p.y;
range = [p.x, p.x + p.width];
} else if (type === "heatmap") {
xValue = p.x;
yValue = p.y;
Expand All @@ -52,29 +53,31 @@ export default function PlotlyView(props) {

const eventHandlerOperator = view[snakeCase(event)];

const defaultParams = {
path: props.path,
relative_path: props.relativePath,
schema: props.schema,
view,
event,
};

if (eventHandlerOperator) {
let params = {};
if (event === "onClick") {
params = {
event,
data,
x_data_source,
...defaultParams,
range,
type: view.type,
x: xValue,
y: yValue,
};
} else if (event === "onSelected") {
params = {
event,
...defaultParams,
data,
type: view.type,
path,
};
}
params = {
...params,
path,
};

triggerPanelEvent(panelId, {
operator: eventHandlerOperator,
params,
Expand Down Expand Up @@ -143,8 +146,8 @@ export default function PlotlyView(props) {
return merge({}, configDefaults, config);
}, [configDefaults, config]);
const mergedData = useMemo(() => {
return mergeData(data, dataDefaults);
}, [data, dataDefaults]);
return mergeData(data || schema?.view?.data, dataDefaults);
}, [data, dataDefaults, schema?.view?.data]);

return (
<Box
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function SliderView(props) {
valueLabelDisplay="auto"
defaultValue={data}
onChange={(e, value: string) => {
onChange(path, type === "number" ? parseFloat(value) : value);
onChange(path, type === "number" ? parseFloat(value) : value, schema);
setUserChanged();
}}
ref={sliderRef}
Expand Down
1 change: 1 addition & 0 deletions app/packages/core/src/plugins/SchemaIO/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ export { default as TupleView } from "./TupleView";
export { default as UnsupportedView } from "./UnsupportedView";
export { default as LazyFieldView } from "./LazyFieldView";
export { default as GridView } from "./GridView";
export { default as DashboardView } from "./DashboardView";
export { default as ArrowNavView } from "./ArrowNavView";
Loading
Loading