Skip to content

Commit

Permalink
Merge pull request #4557 from voxel51/dashboard-comp
Browse files Browse the repository at this point in the history
DashboardView
  • Loading branch information
ritch authored Aug 2, 2024
2 parents fd846d2 + 9f558b2 commit 065aeff
Show file tree
Hide file tree
Showing 12 changed files with 455 additions and 55 deletions.
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>
);
};
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>
);
};

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
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 }) => ({
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}
/>
);
});

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

0 comments on commit 065aeff

Please sign in to comment.