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
1 change: 1 addition & 0 deletions app/packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,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 Down
230 changes: 230 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,230 @@
import {
Box,
BoxProps,
Typography,
useTheme,
styled,
IconButton,
Paper,
Grid,
} from "@mui/material";
import React, { useState, useEffect, useCallback } from "react";
import { Button, HeaderView } from ".";
import { getComponentProps, getPath, getProps } from "../utils";
import { ObjectSchemaType, ViewPropsType } from "../utils/types";
import DynamicIO from "./DynamicIO";
import GridLayout from "react-grid-layout";
import CloseIcon from "@mui/icons-material/Close";
import "react-grid-layout/css/styles.css";
import "react-resizable/css/styles.css";
import usePanelEvent from "@fiftyone/operators/src/usePanelEvent";
import { usePanelId } from "@fiftyone/spaces";

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 = [];

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_close_item) {
triggerPanelEvent(panelId, {
operator: schema.view.on_close_item,
params: { id, path },
});
}
},
[panelId, props, schema.view.on_close_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 ResizeHandle = styled("span")(({ theme }) => ({
ritch marked this conversation as resolved.
Show resolved Hide resolved
position: "absolute",
width: 20,
height: 20,
bottom: 0,
right: 0,
backgroundColor: theme.palette.secondary.main,
borderRadius: "50%",
cursor: "se-resize",
}));

if (!propertiesAsArray.length) {
return <AddItemCTA onAdd={onAddItem} />;
}
const finalLayout = [
...gridLayout,
{ i: "add-item", x: 0, y: ROWS, w: COLS, h: 1, static: true },
];
return (
<Box
{...getComponentProps(props, "container")}
sx={{ position: "relative", marginLeft: -0.5 }}
ritch marked this conversation as resolved.
Show resolved Hide resolved
ritch marked this conversation as resolved.
Show resolved Hide resolved
>
<Box
{...getProps(props, "grid", baseGridProps)}
sx={{ position: "relative" }}
>
<GridLayout
onLayoutChange={handleLayoutChange}
layout={finalLayout}
cols={COLS}
rowHeight={GRID_HEIGHT / ROWS} // Dynamic row height
width={GRID_WIDTH}
onDragStart={() => setIsDragging(true)}
onDragStop={() => setIsDragging(false)}
resizeHandles={["ne"]}
isDraggable={!isDragging}
isResizable={!isDragging} // Allow resizing
draggableHandle=".drag-handle"
resizeHandle={(axis, ref) => {
return <ResizeHandle {...{ axis, 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>
<IconButton
size="small"
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
onCloseItem({ id, path: getPath(path, id) });
}}
sx={{ color: theme.palette.text.secondary }}
>
<CloseIcon />
</IconButton>
</DragHandle>
<DynamicIO
{...props}
schema={property}
path={itemPath}
data={data?.[id]}
parentSchema={schema}
relativePath={id}
/>
</Box>
);
})}
</GridLayout>
</Box>
<AddItemButton key="add-item" onAddItem={onAddItem} />
</Box>
);
}
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 IconButtonView } from "./IconButtonView";
22 changes: 17 additions & 5 deletions app/packages/operators/src/built-in-operators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import {
import * as fos from "@fiftyone/state";
import * as types from "./types";

import { useTrackEvent } from "@fiftyone/analytics";
import { LOAD_WORKSPACE_OPERATOR } from "@fiftyone/spaces/src/components/Workspaces/constants";
import { toSlug } from "@fiftyone/utilities";
import copyToClipboard from "copy-to-clipboard";
import { merge } from "lodash";
import { useSetRecoilState, useRecoilCallback } from "recoil";
import { merge, set as setValue } from "lodash";
import { useRecoilCallback, useSetRecoilState } from "recoil";
import { useOperatorExecutor } from ".";
import useRefetchableSavedViews from "../../core/src/hooks/useRefetchableSavedViews";
import registerPanel from "./Panel/register";
Expand All @@ -30,7 +31,6 @@ import {
} from "./operators";
import { useShowOperatorIO } from "./state";
import usePanelEvent from "./usePanelEvent";
import { useTrackEvent } from "@fiftyone/analytics";

//
// BUILT-IN OPERATORS
Expand Down Expand Up @@ -885,15 +885,27 @@ class PatchPanelData extends Operator {

function useUpdatePanelStatePartial(local?: boolean) {
const setPanelStateById = useSetPanelStateById(local);
return (ctx, { targetPartial = "state", targetParam, patch, clear }) => {
return (
ctx,
{ targetPartial = "state", targetParam, patch, clear, deepMerge, set }
) => {
targetParam = targetParam || targetPartial;
setTimeout(() => {
setPanelStateById(ctx.getCurrentPanelId(), (current = {}) => {
const currentCustomPanelState = current?.[targetPartial] || {};
let updatedState;
const param = ctx.params[targetParam];
if (patch) {
if (set) {
// go through each "param" which is a path and set it in the state
for (let [path, value] of Object.entries(param)) {
updatedState = { ...currentCustomPanelState };
setValue(updatedState, path, value);
}
} else if (deepMerge) {
updatedState = merge({}, currentCustomPanelState, param);
} else if (patch) {
// patch = shallow merge
updatedState = { ...currentCustomPanelState, ...param };
} else if (clear) {
updatedState = {};
} else {
Expand Down
32 changes: 13 additions & 19 deletions app/packages/operators/src/useCustomPanelHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,19 @@ export function useCustomPanelHooks(props: CustomPanelProps): CustomPanelHooks {
}, [panelStateLocal?.loaded]);

const onLoad = useCallback(() => {
if (props.onLoad) {
executeOperator(props.onLoad, {
panel_id: panelId,
panel_state: panelState?.state,
});
if (props.onLoad && !isLoaded) {
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

executeOperator(
props.onLoad,
{ panel_id: panelId, panel_state: panelState?.state },
{
callback(result) {
const { error: onLoadError } = result;
setPanelStateLocal((s) => ({ ...s, onLoadError, loaded: true }));
},
}
);
}
}, [props.onLoad, panelId, panelState?.state]);
}, [props.onLoad, panelId, panelState?.state, isLoaded, setPanelStateLocal]);
useCtxChangePanelEvent(
isLoaded,
panelId,
Expand Down Expand Up @@ -131,19 +137,7 @@ export function useCustomPanelHooks(props: CustomPanelProps): CustomPanelHooks {
);

useEffect(() => {
if (props.onLoad && !isLoaded) {
executeOperator(
props.onLoad,
{ panel_id: panelId },
{
callback(result) {
const { error: onLoadError } = result;
setPanelStateLocal((s) => ({ ...s, onLoadError, loaded: true }));
},
}
);
}

onLoad();
Copy link
Contributor

Choose a reason for hiding this comment

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

At line 145, should we remove props.onLoad, isLoaded , and setPanelStateLocal and add onLoad?

return () => {
if (props.onUnLoad)
executeOperator(props.onUnLoad, { panel_id: panelId });
Expand Down
Loading