From 61538ba0d6a65ca7dfdd01cb39732651096fc183 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Mon, 8 Jul 2024 11:17:53 -0700 Subject: [PATCH 01/12] init dashboard component --- app/packages/core/package.json | 1 + .../SchemaIO/components/DashboardView.tsx | 170 ++++++++++++++++++ .../src/plugins/SchemaIO/components/index.ts | 1 + app/yarn.lock | 41 ++++- fiftyone/operators/types.py | 14 ++ 5 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx diff --git a/app/packages/core/package.json b/app/packages/core/package.json index 082d7410d7..d550c2a2a2 100644 --- a/app/packages/core/package.json +++ b/app/packages/core/package.json @@ -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", diff --git a/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx new file mode 100644 index 0000000000..bcf7784019 --- /dev/null +++ b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx @@ -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 + 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 ResizeHandle = styled("span")(({ theme }) => ({ + position: "absolute", + width: 20, + height: 20, + bottom: 0, + right: 0, + backgroundColor: theme.palette.secondary.main, + borderRadius: "50%", + cursor: "se-resize", + })); + + console.log("viewLayout", viewLayout); + console.log("propertiesAsArray", propertiesAsArray); + + return ( + + + 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 ( + + + {property.title || id} + e.stopPropagation()} + onClick={(e) => { + e.stopPropagation(); + onCloseItem({ id, path: getPath(path, id) }); + }} + sx={{ color: theme.palette.text.secondary }} + > + + + + + + + ); + })} + + + + ); +} diff --git a/app/packages/core/src/plugins/SchemaIO/components/index.ts b/app/packages/core/src/plugins/SchemaIO/components/index.ts index acfa3bb4de..a1fd9a4f9c 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/index.ts +++ b/app/packages/core/src/plugins/SchemaIO/components/index.ts @@ -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"; diff --git a/app/yarn.lock b/app/yarn.lock index 119f88ceac..8f60e960c6 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -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 @@ -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" @@ -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: @@ -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: @@ -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" @@ -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" diff --git a/fiftyone/operators/types.py b/fiftyone/operators/types.py index 6b8e55bb5f..873e20de5d 100644 --- a/fiftyone/operators/types.py +++ b/fiftyone/operators/types.py @@ -336,6 +336,13 @@ 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`.""" + 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. @@ -1995,6 +2002,13 @@ def to_json(self): } +class DashboardView(View): + """Defines a Dashboard view.""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + class DrawerView(View): """Renders an operator prompt as a left or right side drawer. From d6d08a77eebde433214322a11e17b84cd091d241 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Mon, 8 Jul 2024 12:09:54 -0700 Subject: [PATCH 02/12] refactor CustomPanel onload --- .../operators/src/useCustomPanelHooks.ts | 32 ++++++++----------- fiftyone/operators/types.py | 24 ++++++++++++-- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/app/packages/operators/src/useCustomPanelHooks.ts b/app/packages/operators/src/useCustomPanelHooks.ts index 4ca1f845ac..93bf076023 100644 --- a/app/packages/operators/src/useCustomPanelHooks.ts +++ b/app/packages/operators/src/useCustomPanelHooks.ts @@ -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) { + 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, @@ -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(); return () => { if (props.onUnLoad) executeOperator(props.onUnLoad, { panel_id: panelId }); diff --git a/fiftyone/operators/types.py b/fiftyone/operators/types.py index 873e20de5d..0d38938ce0 100644 --- a/fiftyone/operators/types.py +++ b/fiftyone/operators/types.py @@ -337,7 +337,19 @@ def grid(self, name, **kwargs): return obj def dashboard(self, name, **kwargs): - """Defines a dashboard view as a :class:`View`.""" + """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) @@ -350,6 +362,8 @@ def plot(self, name, **kwargs): 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() @@ -2003,7 +2017,13 @@ def to_json(self): class DashboardView(View): - """Defines a Dashboard view.""" + """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) From a61215edafa6a7a55990c870743d0c8d212d56e0 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 9 Jul 2024 14:20:49 -0700 Subject: [PATCH 03/12] fix state issue --- .../SchemaIO/components/DashboardView.tsx | 10 ++++++--- .../operators/src/built-in-operators.ts | 22 ++++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx index bcf7784019..0ebd184aa9 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx @@ -7,7 +7,7 @@ import { IconButton, } from "@mui/material"; import React, { useState, useEffect, useCallback } from "react"; -import { HeaderView } from "."; +import { Button, HeaderView } from "."; import { getComponentProps, getPath, getProps } from "../utils"; import { ObjectSchemaType, ViewPropsType } from "../utils/types"; import DynamicIO from "./DynamicIO"; @@ -117,9 +117,13 @@ export default function DashboardView(props: ViewPropsType) { width={GRID_WIDTH} onDragStart={() => setIsDragging(true)} onDragStop={() => setIsDragging(false)} + resizeHandles={["ne"]} isDraggable={!isDragging} isResizable={!isDragging} // Allow resizing - draggableHandle=".drag-handle" // Specify the drag handle class + draggableHandle=".drag-handle" + resizeHandle={(axis, ref) => { + return ; + }} > {propertiesAsArray.map((property) => { const { id } = property; @@ -159,10 +163,10 @@ export default function DashboardView(props: ViewPropsType) { parentSchema={schema} relativePath={id} /> - ); })} + diff --git a/app/packages/operators/src/built-in-operators.ts b/app/packages/operators/src/built-in-operators.ts index 535d80c88a..e56c6bc27b 100644 --- a/app/packages/operators/src/built-in-operators.ts +++ b/app/packages/operators/src/built-in-operators.ts @@ -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"; @@ -30,7 +31,6 @@ import { } from "./operators"; import { useShowOperatorIO } from "./state"; import usePanelEvent from "./usePanelEvent"; -import { useTrackEvent } from "@fiftyone/analytics"; // // BUILT-IN OPERATORS @@ -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 { From 7d3d0b76058e7ca61ed5222ece19cf5eae407281 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 9 Jul 2024 16:13:56 -0700 Subject: [PATCH 04/12] add button for dashboard --- .../SchemaIO/components/DashboardView.tsx | 66 +++++++++++++++++-- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx index 0ebd184aa9..bdd0d2cd00 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx @@ -5,6 +5,8 @@ import { useTheme, styled, IconButton, + Paper, + Grid, } from "@mui/material"; import React, { useState, useEffect, useCallback } from "react"; import { Button, HeaderView } from "."; @@ -18,6 +20,49 @@ import "react-resizable/css/styles.css"; import usePanelEvent from "@fiftyone/operators/src/usePanelEvent"; import { usePanelId } from "@fiftyone/spaces"; +const AddItemCTA = ({ onAdd }) => { + return ( + + + + Add an Item to Your Dashboard + + + + + ); +}; +const AddItemButton = ({ onAddItem }) => { + return ( + + + + + + + + ); +}; + export default function DashboardView(props: ViewPropsType) { const { schema, path, data, layout } = props; const { properties } = schema as ObjectSchemaType; @@ -40,6 +85,13 @@ export default function DashboardView(props: ViewPropsType) { }, [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) { @@ -97,9 +149,13 @@ export default function DashboardView(props: ViewPropsType) { cursor: "se-resize", })); - console.log("viewLayout", viewLayout); - console.log("propertiesAsArray", propertiesAsArray); - + if (!propertiesAsArray.length) { + return ; + } + const finalLayout = [ + ...gridLayout, + { i: "add-item", x: 0, y: ROWS, w: COLS, h: 1, static: true }, + ]; return ( ); })} - + ); } From 06b0975e404bd563622cbfe77c647e0ab1245041 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Wed, 10 Jul 2024 10:35:04 -0700 Subject: [PATCH 05/12] editable mode for dashboards --- .../SchemaIO/components/DashboardView.tsx | 31 ++++++++++++------- .../SchemaIO/components/SliderView.tsx | 2 +- fiftyone/operators/types.py | 9 ++++++ 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx index bdd0d2cd00..3d4c16a5e0 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx @@ -67,6 +67,8 @@ 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] }); @@ -150,6 +152,9 @@ export default function DashboardView(props: ViewPropsType) { })); if (!propertiesAsArray.length) { + if (!allow_addition) { + return null; + } return ; } const finalLayout = [ @@ -199,17 +204,19 @@ export default function DashboardView(props: ViewPropsType) { > {property.title || id} - e.stopPropagation()} - onClick={(e) => { - e.stopPropagation(); - onCloseItem({ id, path: getPath(path, id) }); - }} - sx={{ color: theme.palette.text.secondary }} - > - - + {allow_deletion && ( + e.stopPropagation()} + onClick={(e) => { + e.stopPropagation(); + onCloseItem({ id, path: getPath(path, id) }); + }} + sx={{ color: theme.palette.text.secondary }} + > + + + )} - + {allow_addition && } ); } diff --git a/app/packages/core/src/plugins/SchemaIO/components/SliderView.tsx b/app/packages/core/src/plugins/SchemaIO/components/SliderView.tsx index d41509b4d2..562b9c17f4 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/SliderView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/SliderView.tsx @@ -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} diff --git a/fiftyone/operators/types.py b/fiftyone/operators/types.py index 0d38938ce0..c29b8f1cb2 100644 --- a/fiftyone/operators/types.py +++ b/fiftyone/operators/types.py @@ -2027,6 +2027,15 @@ class DashboardView(View): def __init__(self, **kwargs): super().__init__(**kwargs) + self.allow_addition = kwargs.get("allow_addition", True) + self.allow_deletion = kwargs.get("allow_deletion", True) + + def to_json(self): + return { + **super().to_json(), + "allow_addition": self.allow_addition, + "allow_deletion": self.allow_deletion, + } class DrawerView(View): From fe6a4c51018531d3f6b34693cab04c934fff1fde Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 16 Jul 2024 16:23:11 -0700 Subject: [PATCH 06/12] minor cleanup --- .../core/src/plugins/SchemaIO/components/DashboardView.tsx | 6 +++--- app/packages/operators/src/built-in-operators.ts | 1 + fiftyone/operators/executor.py | 2 -- fiftyone/operators/types.py | 7 ++++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx index 3d4c16a5e0..95a57371ab 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx @@ -78,14 +78,14 @@ export default function DashboardView(props: ViewPropsType) { const onCloseItem = useCallback( ({ id, path }) => { - if (schema.view.on_close_item) { + if (schema.view.on_remove_item) { triggerPanelEvent(panelId, { - operator: schema.view.on_close_item, + operator: schema.view.on_remove_item, params: { id, path }, }); } }, - [panelId, props, schema.view.on_close_item, triggerPanelEvent] + [panelId, props, schema.view.on_remove_item, triggerPanelEvent] ); const onAddItem = useCallback(() => { if (schema.view.on_add_item) { diff --git a/app/packages/operators/src/built-in-operators.ts b/app/packages/operators/src/built-in-operators.ts index e56c6bc27b..c3f6be413e 100644 --- a/app/packages/operators/src/built-in-operators.ts +++ b/app/packages/operators/src/built-in-operators.ts @@ -847,6 +847,7 @@ class SetPanelState extends Operator { return { updatePanelState: useUpdatePanelStatePartial() }; } async execute(ctx: ExecutionContext): Promise { + debugger; ctx.hooks.updatePanelState(ctx, { targetPartial: "state" }); } } diff --git a/fiftyone/operators/executor.py b/fiftyone/operators/executor.py index a9a740c467..828d8cbc4e 100644 --- a/fiftyone/operators/executor.py +++ b/fiftyone/operators/executor.py @@ -654,7 +654,6 @@ def prompt( params=None, on_success=None, on_error=None, - on_cancel=None, ): """Prompts the user to execute the operator with the given URI. @@ -663,7 +662,6 @@ def prompt( params (None): a dictionary of parameters for the operator on_success (None): a callback to invoke if the user successfully executes the operator on_error (None): a callback to invoke if the execution fails - on_cancel (None): a callback to invoke if the user cancels the operation Returns: a :class:`fiftyone.operators.message.GeneratedMessage` containing diff --git a/fiftyone/operators/types.py b/fiftyone/operators/types.py index c29b8f1cb2..da6958d7cb 100644 --- a/fiftyone/operators/types.py +++ b/fiftyone/operators/types.py @@ -1858,13 +1858,13 @@ class PromptView(View): import fiftyone.operators.types as types # in resolve_input - prompt = types.Prompt( + prompt = types.PromptView( label="This is the title", submit_button_label="Click me", cancel_button_label="Abort" ) inputs = types.Object() - inputs.str("message", label="Message") + inputs.md("Hello world!") return types.Property(inputs, view=prompt) Args: @@ -2022,7 +2022,8 @@ class DashboardView(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 + on_add_item (None): event triggered when an item is added + on_remove_item (None): event triggered when an item is closed """ def __init__(self, **kwargs): From 7e1a4975744fe17d6ef70da7df2f6a98250e70f0 Mon Sep 17 00:00:00 2001 From: imanjra Date: Wed, 17 Jul 2024 11:20:40 -0400 Subject: [PATCH 07/12] fix resize handles for dashboard items --- app/packages/core/package.json | 1 + .../SchemaIO/components/DashboardView.tsx | 116 +++++++++++++----- app/yarn.lock | 10 ++ 3 files changed, 93 insertions(+), 34 deletions(-) diff --git a/app/packages/core/package.json b/app/packages/core/package.json index d550c2a2a2..299320b436 100644 --- a/app/packages/core/package.json +++ b/app/packages/core/package.json @@ -57,6 +57,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", diff --git a/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx index 95a57371ab..b82007fff4 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx @@ -1,24 +1,25 @@ +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, - Typography, - useTheme, - styled, + Grid, IconButton, Paper, - Grid, + styled, + Typography, } 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 React, { forwardRef, useCallback, useState } from "react"; 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"; +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 ( @@ -140,17 +141,6 @@ export default function DashboardView(props: ViewPropsType) { alignItems: "center", })); - const ResizeHandle = styled("span")(({ theme }) => ({ - position: "absolute", - width: 20, - height: 20, - bottom: 0, - right: 0, - backgroundColor: theme.palette.secondary.main, - borderRadius: "50%", - cursor: "se-resize", - })); - if (!propertiesAsArray.length) { if (!allow_addition) { return null; @@ -178,12 +168,12 @@ export default function DashboardView(props: ViewPropsType) { width={GRID_WIDTH} onDragStart={() => setIsDragging(true)} onDragStop={() => setIsDragging(false)} - resizeHandles={["ne"]} + resizeHandles={["e", "w", "n", "s"]} isDraggable={!isDragging} isResizable={!isDragging} // Allow resizing draggableHandle=".drag-handle" resizeHandle={(axis, ref) => { - return ; + return ; }} > {propertiesAsArray.map((property) => { @@ -212,20 +202,22 @@ export default function DashboardView(props: ViewPropsType) { e.stopPropagation(); onCloseItem({ id, path: getPath(path, id) }); }} - sx={{ color: theme.palette.text.secondary }} + sx={{ color: theme.text.secondary }} > )} - + + + ); })} @@ -235,3 +227,59 @@ export default function DashboardView(props: ViewPropsType) { ); } + +const DashboardItemResizeHandle = forwardRef((props, ref) => { + const theme = useTheme(); + const { axis } = props; + + const axisSx = AXIS_SX[axis] || {}; + + return ( + + ); +}); + +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", + }, +}; diff --git a/app/yarn.lock b/app/yarn.lock index 8f60e960c6..09b2072093 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -2399,6 +2399,7 @@ __metadata: "@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 @@ -6543,6 +6544,15 @@ __metadata: languageName: node linkType: hard +"@types/react-grid-layout@npm:^1.3.5": + version: 1.3.5 + resolution: "@types/react-grid-layout@npm:1.3.5" + dependencies: + "@types/react": "*" + checksum: 59e92cac78712bf527e8c9147f70c121a9a7f6faaf1ee5c979c38a9e9445148f5003d6a37a769b6695fdca65e71567033858fc0e1371937c79dcb35f4b36ae5e + languageName: node + linkType: hard + "@types/react-input-autosize@npm:^2.2.1": version: 2.2.4 resolution: "@types/react-input-autosize@npm:2.2.4" From 6e580922a58341ff586fb3887c27ff8f995d8bb7 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Wed, 24 Jul 2024 16:09:31 -0700 Subject: [PATCH 08/12] fix panel state and data ops --- .../operators/src/built-in-operators.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/app/packages/operators/src/built-in-operators.ts b/app/packages/operators/src/built-in-operators.ts index c3f6be413e..c836e94932 100644 --- a/app/packages/operators/src/built-in-operators.ts +++ b/app/packages/operators/src/built-in-operators.ts @@ -15,7 +15,7 @@ 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, set as setValue } from "lodash"; +import { cloneDeep, merge, set as setValue } from "lodash"; import { useRecoilCallback, useSetRecoilState } from "recoil"; import { useOperatorExecutor } from "."; import useRefetchableSavedViews from "../../core/src/hooks/useRefetchableSavedViews"; @@ -847,8 +847,7 @@ class SetPanelState extends Operator { return { updatePanelState: useUpdatePanelStatePartial() }; } async execute(ctx: ExecutionContext): Promise { - debugger; - ctx.hooks.updatePanelState(ctx, { targetPartial: "state" }); + ctx.hooks.updatePanelState(ctx, { targetPartial: "state", set: true }); } } @@ -864,7 +863,7 @@ class SetPanelData extends Operator { return { updatePanelState: useUpdatePanelStatePartial(true) }; } async execute(ctx: ExecutionContext): Promise { - ctx.hooks.updatePanelState(ctx, { targetPartial: "data" }); + ctx.hooks.updatePanelState(ctx, { targetPartial: "data", set: true }); } } @@ -895,22 +894,22 @@ function useUpdatePanelStatePartial(local?: boolean) { setPanelStateById(ctx.getCurrentPanelId(), (current = {}) => { const currentCustomPanelState = current?.[targetPartial] || {}; let updatedState; - const param = ctx.params[targetParam]; + const providedData = ctx.params[targetParam]; 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); - } + // set = replace entire state + updatedState = providedData; } else if (deepMerge) { - updatedState = merge({}, currentCustomPanelState, param); + updatedState = merge({}, currentCustomPanelState, providedData); } else if (patch) { - // patch = shallow merge - updatedState = { ...currentCustomPanelState, ...param }; + updatedState = cloneDeep(currentCustomPanelState); + // patch = shallow merge OR set by path + for (let [path, value] of Object.entries(providedData)) { + setValue(updatedState, path, value); + } } else if (clear) { updatedState = {}; } else { - updatedState = param; + throw new Error("useUpdatePanelStatePartial: Invalid operation"); } return { ...current, [targetPartial]: updatedState }; @@ -977,6 +976,7 @@ class ShowPanelOutput extends Operator { ctx.hooks.updatePanelState(ctx, { targetPartial: "schema", targetParam: "output", + set: true, }); } } From acb91dd95942efa15a2543d44787fda010a55a6a Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Wed, 24 Jul 2024 16:10:37 -0700 Subject: [PATCH 09/12] cleanup panel state ops docs --- fiftyone/operators/operations.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fiftyone/operators/operations.py b/fiftyone/operators/operations.py index 28d4804fca..680ebea1a7 100644 --- a/fiftyone/operators/operations.py +++ b/fiftyone/operators/operations.py @@ -168,11 +168,12 @@ def clear_panel_data(self, panel_id=None): ) def set_panel_state(self, state, panel_id=None): - """Set the state of the specified panel in the App. + """Set the entire state of the specified panel in the App. Args: panel_id (None): the optional ID of the panel to clear. If not provided, the ctx.current_panel.id will be used. + state (dict): the state to set """ return self._ctx.trigger( "set_panel_state", @@ -180,11 +181,12 @@ def set_panel_state(self, state, panel_id=None): ) def set_panel_data(self, data, panel_id=None): - """Set the data of the specified panel in the App. + """Set the entire data of the specified panel in the App. Args: panel_id (None): the optional ID of the panel to clear. If not provided, the ctx.current_panel.id will be used. + data (dict): the data to set """ return self._ctx.trigger( "set_panel_data", From 2358d837e53841f053dd1d834190a78c23e46fc3 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Wed, 24 Jul 2024 16:11:18 -0700 Subject: [PATCH 10/12] add batch_set_data util for panels --- fiftyone/operators/panel.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/fiftyone/operators/panel.py b/fiftyone/operators/panel.py index 0608baca0b..b44ddfebab 100644 --- a/fiftyone/operators/panel.py +++ b/fiftyone/operators/panel.py @@ -199,7 +199,7 @@ class PanelRefData(PanelRefBase): Class representing the data of a panel. """ - def set(self, key, value): + def set(self, key, value, _exec_op=True): """ Sets the data of the panel. @@ -210,7 +210,8 @@ def set(self, key, value): super().set(key, value) args = {} pydash.set_(args, key, value) - self._ctx.ops.patch_panel_data(args) + if _exec_op: + self._ctx.ops.patch_panel_data(args) def get(self, key, default=None): raise WriteOnlyError("Panel data is write-only") @@ -278,11 +279,22 @@ def set_data(self, key, value): Sets the data of the panel. Args: - key (str): The data key. + path (str): The dot delimited path to set. value (any): The data value. """ self._data.set(key, value) + def batch_set_data(self, data): + """ + Sets multiple data values by path. + + Args: + data (dict): A dictionary of key-value pairs. Where the key is the path and the value is the data value. + """ + for key, value in data.items(): + self._data.set(key, value, _exec_op=False) + self._ctx.ops.patch_panel_data(data) + def set_title(self, title): """ Sets the title of the panel. From 830c7894e17b1c13075f7241c7f567666d67d820 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Mon, 29 Jul 2024 14:38:45 -0700 Subject: [PATCH 11/12] dashboard improvements --- .../SchemaIO/components/DashboardView.tsx | 17 ++++++++--------- .../plugins/SchemaIO/components/PlotlyView.tsx | 4 +++- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx index b82007fff4..f91cec1738 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/DashboardView.tsx @@ -140,6 +140,8 @@ export default function DashboardView(props: ViewPropsType) { justifyContent: "space-between", alignItems: "center", })); + const [showGrid, setShowGrid] = useState(false); + const toggleGrid = useCallback(() => setShowGrid(!showGrid), [showGrid]); if (!propertiesAsArray.length) { if (!allow_addition) { @@ -151,15 +153,11 @@ export default function DashboardView(props: ViewPropsType) { ...gridLayout, { i: "add-item", x: 0, y: ROWS, w: COLS, h: 1, static: true }, ]; + return ( - - + + {!showGrid && } + {showGrid && ( - + )} {allow_addition && } ); diff --git a/app/packages/core/src/plugins/SchemaIO/components/PlotlyView.tsx b/app/packages/core/src/plugins/SchemaIO/components/PlotlyView.tsx index 4bdfe75187..e8665016b4 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/PlotlyView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/PlotlyView.tsx @@ -129,7 +129,9 @@ export default function PlotlyView(props) { const mergedLayout = merge({}, layoutDefaults, layout); const mergedConfig = merge({}, configDefaults, config); - const mergedData = mergeData(data, dataDefaults); + const mergedData = mergeData(data || schema?.view?.data, dataDefaults); + + console.log(mergedData); return ( Date: Thu, 1 Aug 2024 11:36:42 -0700 Subject: [PATCH 12/12] fix scatter plot events --- .../SchemaIO/components/PlotlyView.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/app/packages/core/src/plugins/SchemaIO/components/PlotlyView.tsx b/app/packages/core/src/plugins/SchemaIO/components/PlotlyView.tsx index e8665016b4..2ad3cb35c5 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/PlotlyView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/PlotlyView.tsx @@ -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") { @@ -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; @@ -52,25 +53,30 @@ 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, }; } + triggerPanelEvent(panelId, { operator: eventHandlerOperator, params,