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
170 changes: 170 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,170 @@
import {
Box,
BoxProps,
Typography,
useTheme,
styled,
IconButton,
} from "@mui/material";
import React, { useState, useEffect, useCallback } from "react";
import { 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";

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 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",
}));

console.log("viewLayout", viewLayout);
ritch marked this conversation as resolved.
Show resolved Hide resolved
console.log("propertiesAsArray", propertiesAsArray);

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={gridLayout}
cols={COLS}
rowHeight={GRID_HEIGHT / ROWS} // Dynamic row height
width={GRID_WIDTH}
onDragStart={() => setIsDragging(true)}
onDragStop={() => setIsDragging(false)}
isDraggable={!isDragging}
isResizable={!isDragging} // Allow resizing
draggableHandle=".drag-handle" // Specify the drag handle class
>
{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}
/>
<ResizeHandle className="react-resizable-handle" />
</Box>
);
})}
</GridLayout>
</Box>
</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";
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
41 changes: 39 additions & 2 deletions app/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2421,6 +2421,7 @@ __metadata:
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 Expand Up @@ -10670,6 +10671,13 @@ __metadata:
languageName: node
linkType: hard

"fast-equals@npm:^4.0.3":
version: 4.0.3
resolution: "fast-equals@npm:4.0.3"
checksum: 3d5935b757f9f2993e59b5164a7a9eeda0de149760495375cde14a4ed725186a7e6c1c0d58f7d42d2f91deb97f3fce1e0aad5591916ef0984278199a85c87c87
languageName: node
linkType: hard

"fast-equals@npm:^5.0.1":
version: 5.0.1
resolution: "fast-equals@npm:5.0.1"
Expand Down Expand Up @@ -16118,7 +16126,7 @@ __metadata:
languageName: node
linkType: hard

"prop-types@npm:^15.0.0, prop-types@npm:^15.5.10, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
"prop-types@npm:15.x, prop-types@npm:^15.0.0, prop-types@npm:^15.5.10, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.1, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
dependencies:
Expand Down Expand Up @@ -16319,7 +16327,7 @@ __metadata:
languageName: node
linkType: hard

"react-draggable@npm:^4.4.5":
"react-draggable@npm:^4.0.3, react-draggable@npm:^4.4.5":
version: 4.4.6
resolution: "react-draggable@npm:4.4.6"
dependencies:
Expand Down Expand Up @@ -16379,6 +16387,23 @@ __metadata:
languageName: node
linkType: hard

"react-grid-layout@npm:^1.4.4":
version: 1.4.4
resolution: "react-grid-layout@npm:1.4.4"
dependencies:
clsx: ^2.0.0
fast-equals: ^4.0.3
prop-types: ^15.8.1
react-draggable: ^4.4.5
react-resizable: ^3.0.5
resize-observer-polyfill: ^1.5.1
peerDependencies:
react: ">= 16.3.0"
react-dom: ">= 16.3.0"
checksum: 0d1d27d6ca58d5b7e9bf778f5d74e5a6353737980f86652b6a799a83b8683735d333f2a0d9b48e3186879da3eefd7f53a7db05a5149dfba27d9d124e5cd3f138
languageName: node
linkType: hard

"react-hotkeys@npm:^2.0.0":
version: 2.0.0
resolution: "react-hotkeys@npm:2.0.0"
Expand Down Expand Up @@ -16548,6 +16573,18 @@ __metadata:
languageName: node
linkType: hard

"react-resizable@npm:^3.0.5":
version: 3.0.5
resolution: "react-resizable@npm:3.0.5"
dependencies:
prop-types: 15.x
react-draggable: ^4.0.3
peerDependencies:
react: ">= 16.3"
checksum: 616a10205acfaf8cc3aa0824b60f6d037eef87143d8f338cf826deb74a353db9b9baad67a65dc8535fe90840bfc3e1b8a901f9c247033ffeec2f30405ac7528e
languageName: node
linkType: hard

"react-smooth@npm:^4.0.0":
version: 4.0.1
resolution: "react-smooth@npm:4.0.1"
Expand Down
34 changes: 34 additions & 0 deletions fiftyone/operators/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,13 +336,34 @@ def grid(self, name, **kwargs):
self.define_property(name, obj, view=grid)
return obj

def dashboard(self, name, **kwargs):
"""Defines a dashboard view as a :class:`View`.

Args:
name: the name of the property
layout (None): the layout of the dashboard
on_layout_change (None): event handler for layout change
on_close_item (None): event handler for item close

Returns:
an :class:`Object`

See :class:`DashboardView` for more information.
"""
dashboard = DashboardView(**kwargs)
obj = Object()
self.define_property(name, obj, view=dashboard)
return obj

def plot(self, name, **kwargs):
"""Defines an object property displayed as a plot.

Args:
name: the name of the property
config (None): the chart config
layout (None): the chart layout

See :class:`PlotlyView` for more information.
"""
plot = PlotlyView(**kwargs)
obj = Object()
Expand Down Expand Up @@ -1995,6 +2016,19 @@ def to_json(self):
}


class DashboardView(View):
ritch marked this conversation as resolved.
Show resolved Hide resolved
"""Defines a Dashboard view.

Args:
layout (None): the layout of the dashboard.
on_layout_change (None): event triggered when the layout changes
on_close_item (None): event triggered when an item is closed
"""

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


class DrawerView(View):
"""Renders an operator prompt as a left or right side drawer.

Expand Down