From b01dd09a79c0c70d6c00f725001b8cd048f84f53 Mon Sep 17 00:00:00 2001 From: imanjra Date: Mon, 7 Oct 2024 00:15:45 -0400 Subject: [PATCH 1/7] add spaces context to operators --- app/packages/operators/src/CustomPanel.tsx | 2 ++ app/packages/operators/src/hooks.ts | 6 ++++ app/packages/operators/src/operators.ts | 21 +++++++++++- app/packages/operators/src/state.ts | 10 ++++++ .../operators/src/useCustomPanelHooks.ts | 9 +++++ docs/source/plugins/developing_plugins.rst | 14 ++++++++ fiftyone/operators/builtin.py | 34 +++++-------------- fiftyone/operators/executor.py | 12 +++++++ fiftyone/operators/operations.py | 1 + fiftyone/operators/panel.py | 1 + 10 files changed, 83 insertions(+), 27 deletions(-) diff --git a/app/packages/operators/src/CustomPanel.tsx b/app/packages/operators/src/CustomPanel.tsx index cf9f82a5b3..ae7caa88de 100644 --- a/app/packages/operators/src/CustomPanel.tsx +++ b/app/packages/operators/src/CustomPanel.tsx @@ -115,6 +115,7 @@ export function defineCustomPanel({ on_change_selected_labels, on_change_extended_selection, on_change_group_slice, + on_change_spaces, panel_name, panel_label, }) { @@ -132,6 +133,7 @@ export function defineCustomPanel({ onChangeSelectedLabels={on_change_selected_labels} onChangeExtendedSelection={on_change_extended_selection} onChangeGroupSlice={on_change_group_slice} + onChangeSpaces={on_change_spaces} dimensions={dimensions} panelName={panel_name} panelLabel={panel_label} diff --git a/app/packages/operators/src/hooks.ts b/app/packages/operators/src/hooks.ts index a636f1b263..855c6a2b1c 100644 --- a/app/packages/operators/src/hooks.ts +++ b/app/packages/operators/src/hooks.ts @@ -28,6 +28,8 @@ function useOperatorThrottledContextSetter() { const groupSlice = useRecoilValue(fos.groupSlice); const currentSample = useCurrentSample(); const setContext = useSetRecoilState(operatorThrottledContext); + const spaces = useRecoilValue(fos.sessionSpaces); + const workspaceName = spaces._name; const setThrottledContext = useMemo(() => { return debounce( (context) => { @@ -49,6 +51,8 @@ function useOperatorThrottledContextSetter() { currentSample, viewName, groupSlice, + spaces, + workspaceName, }); }, [ setThrottledContext, @@ -61,6 +65,8 @@ function useOperatorThrottledContextSetter() { currentSample, viewName, groupSlice, + spaces, + workspaceName, ]); } diff --git a/app/packages/operators/src/operators.ts b/app/packages/operators/src/operators.ts index d16f175fba..a96f505406 100644 --- a/app/packages/operators/src/operators.ts +++ b/app/packages/operators/src/operators.ts @@ -1,5 +1,6 @@ import { AnalyticsInfo, usingAnalytics } from "@fiftyone/analytics"; -import { ServerError, getFetchFunction, isNullish } from "@fiftyone/utilities"; +import { SpaceNode, spaceNodeFromJSON, SpaceNodeJSON } from "@fiftyone/spaces"; +import { getFetchFunction, isNullish, ServerError } from "@fiftyone/utilities"; import { CallbackInterface } from "recoil"; import { QueueItemStatus } from "./constants"; import * as types from "./types"; @@ -92,6 +93,8 @@ export type RawContext = { }; groupSlice: string; queryPerformance?: boolean; + spaces: SpaceNodeJSON; + workspaceName: string; }; export class ExecutionContext { @@ -140,6 +143,12 @@ export class ExecutionContext { public get queryPerformance(): boolean { return Boolean(this._currentContext.queryPerformance); } + public get spaces(): SpaceNode { + return spaceNodeFromJSON(this._currentContext.spaces); + } + public get workspaceName(): string { + return this._currentContext.workspaceName; + } getCurrentPanelId(): string | null { return this.params.panel_id || this.currentPanel?.id || null; @@ -548,6 +557,8 @@ async function executeOperatorAsGenerator( view: currentContext.view, view_name: currentContext.viewName, group_slice: currentContext.groupSlice, + spaces: currentContext.spaces, + workspace_name: currentContext.workspaceName, }, "json-stream" ); @@ -712,6 +723,8 @@ export async function executeOperatorWithContext( view_name: currentContext.viewName, group_slice: currentContext.groupSlice, query_performance: currentContext.queryPerformance, + spaces: currentContext.spaces, + workspace_name: currentContext.workspaceName, } ); result = serverResult.result; @@ -815,6 +828,8 @@ export async function resolveRemoteType( view: currentContext.view, view_name: currentContext.viewName, group_slice: currentContext.groupSlice, + spaces: currentContext.spaces, + workspace_name: currentContext.workspaceName, } ); @@ -889,6 +904,8 @@ export async function resolveExecutionOptions( view: currentContext.view, view_name: currentContext.viewName, group_slice: currentContext.groupSlice, + spaces: currentContext.spaces, + workspace_name: currentContext.workspaceName, } ); @@ -920,6 +937,8 @@ export async function fetchRemotePlacements(ctx: ExecutionContext) { current_sample: currentContext.currentSample, view_name: currentContext.viewName, group_slice: currentContext.groupSlice, + spaces: currentContext.spaces, + workspace_name: currentContext.workspaceName, } ); if (result && result.error) { diff --git a/app/packages/operators/src/state.ts b/app/packages/operators/src/state.ts index cf0f08cd14..535f61c0c2 100644 --- a/app/packages/operators/src/state.ts +++ b/app/packages/operators/src/state.ts @@ -95,6 +95,8 @@ const globalContextSelector = selector({ const extendedSelection = get(fos.extendedSelection); const groupSlice = get(fos.groupSlice); const queryPerformance = typeof get(fos.lightningThreshold) === "number"; + const spaces = get(fos.sessionSpaces); + const workspaceName = spaces?._name; return { datasetName, @@ -107,6 +109,8 @@ const globalContextSelector = selector({ extendedSelection, groupSlice, queryPerformance, + spaces, + workspaceName, }; }, }); @@ -148,6 +152,8 @@ const useExecutionContext = (operatorName, hooks = {}) => { extendedSelection, groupSlice, queryPerformance, + spaces, + workspaceName, } = curCtx; const [analyticsInfo] = useAnalyticsInfo(); const ctx = useMemo(() => { @@ -166,6 +172,8 @@ const useExecutionContext = (operatorName, hooks = {}) => { analyticsInfo, groupSlice, queryPerformance, + spaces, + workspaceName, }, hooks ); @@ -182,6 +190,8 @@ const useExecutionContext = (operatorName, hooks = {}) => { currentSample, groupSlice, queryPerformance, + spaces, + workspaceName, ]); return ctx; diff --git a/app/packages/operators/src/useCustomPanelHooks.ts b/app/packages/operators/src/useCustomPanelHooks.ts index afbaf851dd..155f67b98c 100644 --- a/app/packages/operators/src/useCustomPanelHooks.ts +++ b/app/packages/operators/src/useCustomPanelHooks.ts @@ -26,6 +26,8 @@ export interface CustomPanelProps { onChangeSelectedLabels?: string; onChangeExtendedSelection?: string; onChangeGroupSlice?: string; + onChangeSpaces?: string; + onChangeWorkspace?: string; dimensions: DimensionsType | null; panelName?: string; panelLabel?: string; @@ -136,6 +138,13 @@ export function useCustomPanelHooks(props: CustomPanelProps): CustomPanelHooks { ctx.groupSlice, props.onChangeGroupSlice ); + useCtxChangePanelEvent(isLoaded, panelId, ctx.spaces, props.onChangeSpaces); + useCtxChangePanelEvent( + isLoaded, + panelId, + ctx.workspaceName, + props.onChangeWorkspace + ); useEffect(() => { onLoad(); diff --git a/docs/source/plugins/developing_plugins.rst b/docs/source/plugins/developing_plugins.rst index 68b9a1c12a..d0769a5d18 100644 --- a/docs/source/plugins/developing_plugins.rst +++ b/docs/source/plugins/developing_plugins.rst @@ -1001,6 +1001,7 @@ contains the following properties: if any - `ctx.results` - a dict containing the outputs of the `execute()` method, if it has been called +- `ctx.spaces` - The current workspace or the state of spaces in the app. - `ctx.hooks` **(JS only)** - the return value of the operator's `useHooks()` method @@ -2060,6 +2061,19 @@ subsequent sections. ctx.panel.set_state("event", "on_change_group_slice") ctx.panel.set_data("event_data", event) + def on_change_spaces(self, ctx): + """Implement this method to set panel state/data when the current + state of spaces changes in the app. + + The current state of spaces will be available via ``ctx.spaces``. + """ + event = { + "data": ctx.spaces, + "description": "the current state of spaces", + } + ctx.panel.set_state("event", "on_change_spaces") + ctx.panel.set_data("event_data", event) + ####################################################################### # Custom events # These events are defined by user code above and, just like builtin diff --git a/fiftyone/operators/builtin.py b/fiftyone/operators/builtin.py index 12d4ee76bb..c0f163f45d 100644 --- a/fiftyone/operators/builtin.py +++ b/fiftyone/operators/builtin.py @@ -1939,28 +1939,6 @@ def resolve_input(self, ctx): ), ) - # @todo infer this automatically from current App spaces - spaces_prop = inputs.oneof( - "spaces", - [types.String(), types.Object()], - default=None, - required=True, - label="Spaces", - description=( - "JSON description of the workspace to save: " - "`print(session.spaces.to_json(True))`" - ), - view=types.CodeView(), - ) - - spaces = ctx.params.get("spaces", None) - if spaces is not None: - try: - _parse_spaces(spaces) - except: - spaces_prop.invalid = True - spaces_prop.error_message = "Invalid workspace definition" - name = ctx.params.get("name", None) if name in workspaces: @@ -1979,7 +1957,10 @@ def execute(self, ctx): color = ctx.params.get("color", None) spaces = ctx.params.get("spaces", None) - spaces = _parse_spaces(spaces) + if spaces is None: + spaces = ctx.spaces + else: + spaces = _parse_spaces(spaces) ctx.dataset.save_workspace( name, @@ -2034,11 +2015,12 @@ def _edit_workspace_info_inputs(ctx, inputs): for key in workspaces: workspace_selector.add_choice(key, label=key) - # @todo default to current workspace name, if one is currently open + current_workspace = ctx.spaces.name if ctx.spaces else None inputs.enum( "name", workspace_selector.values(), required=True, + default=current_workspace, label="Workspace", description="The workspace to edit", view=workspace_selector, @@ -2102,11 +2084,11 @@ def resolve_input(self, ctx): workspace_selector = types.AutocompleteView() for key in workspaces: workspace_selector.add_choice(key, label=key) - + current_workspace = ctx.spaces.name if ctx.spaces else None inputs.enum( "name", workspace_selector.values(), - default=None, + default=current_workspace, required=True, label="Workspace", description="The workspace to delete", diff --git a/fiftyone/operators/executor.py b/fiftyone/operators/executor.py index e0e6ac784c..7624653a14 100644 --- a/fiftyone/operators/executor.py +++ b/fiftyone/operators/executor.py @@ -718,6 +718,18 @@ def query_performance(self): """Whether query performance is enabled.""" return self.request_params.get("query_performance", None) + @property + def spaces(self): + """The current workspace or the state of spaces in the app.""" + workspace_name = self.request_params.get("workspace_name", None) + if workspace_name is not None: + return self.dataset.load_workspace(workspace_name) + + spaces_dict = self.request_params.get("spaces", None) + if spaces_dict is None: + return None + return fo.Space.from_dict(spaces_dict) + def prompt( self, operator_uri, diff --git a/fiftyone/operators/operations.py b/fiftyone/operators/operations.py index 933f6f3d55..84c5e78e90 100644 --- a/fiftyone/operators/operations.py +++ b/fiftyone/operators/operations.py @@ -356,6 +356,7 @@ def register_panel( current extended selection changes on_change_group_slice (None): an operator to invoke when the group slice changes + on_change_spaces (None): an operator to invoke when the current spaces changes allow_duplicates (False): whether to allow multiple instances of the panel to the opened """ diff --git a/fiftyone/operators/panel.py b/fiftyone/operators/panel.py index b9d897c2fd..1a286e044d 100644 --- a/fiftyone/operators/panel.py +++ b/fiftyone/operators/panel.py @@ -126,6 +126,7 @@ def on_startup(self, ctx): "on_change_selected_labels", "on_change_extended_selection", "on_change_group_slice", + "on_change_spaces", ] for method in methods + ctx_change_events: if hasattr(self, method) and callable(getattr(self, method)): From 979ea73570c211a95f2676851d8e65a97e5ea5ed Mon Sep 17 00:00:00 2001 From: brimoor Date: Mon, 21 Oct 2024 22:17:17 -0400 Subject: [PATCH 2/7] cleanup --- docs/source/plugins/developing_plugins.rst | 28 +++++++++--------- fiftyone/operators/builtin.py | 34 +++++++++++++++------- fiftyone/operators/executor.py | 25 ++++++++-------- fiftyone/operators/operations.py | 13 +++++---- 4 files changed, 59 insertions(+), 41 deletions(-) diff --git a/docs/source/plugins/developing_plugins.rst b/docs/source/plugins/developing_plugins.rst index d0769a5d18..783320f102 100644 --- a/docs/source/plugins/developing_plugins.rst +++ b/docs/source/plugins/developing_plugins.rst @@ -974,6 +974,7 @@ contains the following properties: - `ctx.dataset_name`: the name of the current dataset - `ctx.dataset` - the current |Dataset| instance - `ctx.view` - the current |DatasetView| instance +- `ctx.spaces` - the current :ref:`Spaces layout ` in the App - `ctx.current_sample` - the ID of the active sample in the App modal, if any - `ctx.selected` - the list of currently selected samples in the App, if any - `ctx.selected_labels` - the list of currently selected labels in the App, @@ -1001,7 +1002,6 @@ contains the following properties: if any - `ctx.results` - a dict containing the outputs of the `execute()` method, if it has been called -- `ctx.spaces` - The current workspace or the state of spaces in the app. - `ctx.hooks` **(JS only)** - the return value of the operator's `useHooks()` method @@ -1992,6 +1992,19 @@ subsequent sections. ctx.panel.set_state("event", "on_change_view") ctx.panel.set_data("event_data", event) + def on_change_spaces(self, ctx): + """Implement this method to set panel state/data when the current + spaces layout changes. + + The current spaces layout will be available via ``ctx.spaces``. + """ + event = { + "data": ctx.spaces, + "description": "the current spaces layout", + } + ctx.panel.set_state("event", "on_change_spaces") + ctx.panel.set_data("event_data", event) + def on_change_current_sample(self, ctx): """Implement this method to set panel state/data when a new sample is loaded in the Sample modal. @@ -2061,19 +2074,6 @@ subsequent sections. ctx.panel.set_state("event", "on_change_group_slice") ctx.panel.set_data("event_data", event) - def on_change_spaces(self, ctx): - """Implement this method to set panel state/data when the current - state of spaces changes in the app. - - The current state of spaces will be available via ``ctx.spaces``. - """ - event = { - "data": ctx.spaces, - "description": "the current state of spaces", - } - ctx.panel.set_state("event", "on_change_spaces") - ctx.panel.set_data("event_data", event) - ####################################################################### # Custom events # These events are defined by user code above and, just like builtin diff --git a/fiftyone/operators/builtin.py b/fiftyone/operators/builtin.py index c0f163f45d..57783f0842 100644 --- a/fiftyone/operators/builtin.py +++ b/fiftyone/operators/builtin.py @@ -14,6 +14,7 @@ import fiftyone.core.storage as fos import fiftyone.operators as foo import fiftyone.operators.types as types +from fiftyone.core.odm.workspace import default_workspace_factory class EditFieldInfo(foo.Operator): @@ -1957,10 +1958,11 @@ def execute(self, ctx): color = ctx.params.get("color", None) spaces = ctx.params.get("spaces", None) - if spaces is None: + curr_spaces = spaces is None + if curr_spaces: spaces = ctx.spaces else: - spaces = _parse_spaces(spaces) + spaces = _parse_spaces(ctx, spaces) ctx.dataset.save_workspace( name, @@ -1970,6 +1972,9 @@ def execute(self, ctx): overwrite=True, ) + if curr_spaces: + ctx.ops.set_spaces(name=name) + class EditWorkspaceInfo(foo.Operator): @property @@ -1995,9 +2000,14 @@ def execute(self, ctx): description = ctx.params.get("description", None) color = ctx.params.get("color", None) + curr_name = ctx.spaces.name info = dict(name=new_name, description=description, color=color) + ctx.dataset.update_workspace_info(name, info) + if curr_name is not None and curr_name != new_name: + ctx.ops.set_spaces(name=new_name) + def _edit_workspace_info_inputs(ctx, inputs): workspaces = ctx.dataset.list_workspaces() @@ -2015,12 +2025,11 @@ def _edit_workspace_info_inputs(ctx, inputs): for key in workspaces: workspace_selector.add_choice(key, label=key) - current_workspace = ctx.spaces.name if ctx.spaces else None inputs.enum( "name", workspace_selector.values(), + default=ctx.spaces.name, required=True, - default=current_workspace, label="Workspace", description="The workspace to edit", view=workspace_selector, @@ -2084,11 +2093,11 @@ def resolve_input(self, ctx): workspace_selector = types.AutocompleteView() for key in workspaces: workspace_selector.add_choice(key, label=key) - current_workspace = ctx.spaces.name if ctx.spaces else None + inputs.enum( "name", workspace_selector.values(), - default=current_workspace, + default=ctx.spaces.name, required=True, label="Workspace", description="The workspace to delete", @@ -2109,8 +2118,12 @@ def resolve_input(self, ctx): def execute(self, ctx): name = ctx.params["name"] + curr_spaces = name == ctx.spaces.name ctx.dataset.delete_workspace(name) + if curr_spaces: + ctx.ops.set_spaces(spaces=default_workspace_factory()) + class SyncLastModifiedAt(foo.Operator): @property @@ -2274,10 +2287,11 @@ def _get_non_default_frame_fields(dataset): return schema -def _parse_spaces(spaces): - if isinstance(spaces, dict): - return fo.Space.from_dict(spaces) - return fo.Space.from_json(spaces) +def _parse_spaces(ctx, spaces): + if isinstance(spaces, str): + spaces = json.loads(spaces) + + return fo.Space.from_dict(spaces) BUILTIN_OPERATORS = [ diff --git a/fiftyone/operators/executor.py b/fiftyone/operators/executor.py index 7624653a14..ac83bd3fe6 100644 --- a/fiftyone/operators/executor.py +++ b/fiftyone/operators/executor.py @@ -610,6 +610,19 @@ def has_custom_view(self): ) return has_stages or has_filters or has_extended + @property + def spaces(self): + """The current spaces layout in the FiftyOne App.""" + workspace_name = self.request_params.get("workspace_name", None) + if workspace_name is not None: + return self.dataset.load_workspace(workspace_name) + + spaces_dict = self.request_params.get("spaces", None) + if spaces_dict is not None: + return fo.Space.from_dict(spaces_dict) + + return None + @property def selected(self): """The list of selected sample IDs (if any).""" @@ -718,18 +731,6 @@ def query_performance(self): """Whether query performance is enabled.""" return self.request_params.get("query_performance", None) - @property - def spaces(self): - """The current workspace or the state of spaces in the app.""" - workspace_name = self.request_params.get("workspace_name", None) - if workspace_name is not None: - return self.dataset.load_workspace(workspace_name) - - spaces_dict = self.request_params.get("spaces", None) - if spaces_dict is None: - return None - return fo.Space.from_dict(spaces_dict) - def prompt( self, operator_uri, diff --git a/fiftyone/operators/operations.py b/fiftyone/operators/operations.py index 84c5e78e90..ef31a71c06 100644 --- a/fiftyone/operators/operations.py +++ b/fiftyone/operators/operations.py @@ -310,8 +310,9 @@ def register_panel( on_unload=None, on_change=None, on_change_ctx=None, - on_change_view=None, on_change_dataset=None, + on_change_view=None, + on_change_spaces=None, on_change_current_sample=None, on_change_selected=None, on_change_selected_labels=None, @@ -342,10 +343,12 @@ def register_panel( changes on_change_ctx (None): an operator to invoke when the panel execution context changes - on_change_view (None): an operator to invoke when the current view - changes on_change_dataset (None): an operator to invoke when the current dataset changes + on_change_view (None): an operator to invoke when the current view + changes + on_change_spaces (None): an operator to invoke when the current + spaces layout changes on_change_current_sample (None): an operator to invoke when the current sample changes on_change_selected (None): an operator to invoke when the current @@ -356,7 +359,6 @@ def register_panel( current extended selection changes on_change_group_slice (None): an operator to invoke when the group slice changes - on_change_spaces (None): an operator to invoke when the current spaces changes allow_duplicates (False): whether to allow multiple instances of the panel to the opened """ @@ -373,8 +375,9 @@ def register_panel( "on_unload": on_unload, "on_change": on_change, "on_change_ctx": on_change_ctx, - "on_change_view": on_change_view, "on_change_dataset": on_change_dataset, + "on_change_view": on_change_view, + "on_change_spaces": on_change_spaces, "on_change_current_sample": on_change_current_sample, "on_change_selected": on_change_selected, "on_change_selected_labels": on_change_selected_labels, From 4a786610115c4370713c4c8bd2317cec948899a0 Mon Sep 17 00:00:00 2001 From: brimoor Date: Mon, 21 Oct 2024 22:20:36 -0400 Subject: [PATCH 3/7] one more --- fiftyone/operators/panel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fiftyone/operators/panel.py b/fiftyone/operators/panel.py index 1a286e044d..482519159c 100644 --- a/fiftyone/operators/panel.py +++ b/fiftyone/operators/panel.py @@ -119,14 +119,14 @@ def on_startup(self, ctx): methods = ["on_load", "on_unload", "on_change"] ctx_change_events = [ "on_change_ctx", - "on_change_view", "on_change_dataset", + "on_change_view", + "on_change_spaces", "on_change_current_sample", "on_change_selected", "on_change_selected_labels", "on_change_extended_selection", "on_change_group_slice", - "on_change_spaces", ] for method in methods + ctx_change_events: if hasattr(self, method) and callable(getattr(self, method)): From d7b47c210c954e9c3274c3b79899b6c3601bea14 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 22 Oct 2024 09:55:26 -0700 Subject: [PATCH 4/7] frameloader fixes --- .../SchemaIO/components/FrameLoaderView.tsx | 11 ++---- fiftyone/operators/types.py | 38 +++++++++++++++++-- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/app/packages/core/src/plugins/SchemaIO/components/FrameLoaderView.tsx b/app/packages/core/src/plugins/SchemaIO/components/FrameLoaderView.tsx index ff41d1274e..8c0e94fb1e 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/FrameLoaderView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/FrameLoaderView.tsx @@ -1,9 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import { ObjectSchemaType, ViewPropsType } from "../utils/types"; -import { - DEFAULT_FRAME_NUMBER, - GLOBAL_TIMELINE_ID, -} from "@fiftyone/playback/src/lib/constants"; +import { DEFAULT_FRAME_NUMBER } from "@fiftyone/playback/src/lib/constants"; import { BufferManager, BufferRange } from "@fiftyone/utilities"; import { usePanelEvent } from "@fiftyone/operators"; import { usePanelId, useSetPanelStateById } from "@fiftyone/spaces"; @@ -13,7 +10,7 @@ import _ from "lodash"; export default function FrameLoaderView(props: ViewPropsType) { const { schema, path, data } = props; const { view = {} } = schema; - const { on_load_range, timeline_id, target } = view; + const { on_load_range, target, timeline_name } = view; const panelId = usePanelId(); const triggerEvent = usePanelEvent(); const setPanelState = useSetPanelStateById(true); @@ -76,14 +73,14 @@ export default function FrameLoaderView(props: ViewPropsType) { [data, setPanelState, panelId, target] ); - const { isTimelineInitialized, subscribe } = useTimeline(); + const { isTimelineInitialized, subscribe } = useTimeline(timeline_name); const [subscribed, setSubscribed] = useState(false); React.useEffect(() => { if (subscribed) return; if (isTimelineInitialized) { subscribe({ - id: timeline_id || GLOBAL_TIMELINE_ID, + id: panelId, loadRange, renderFrame: myRenderFrame, }); diff --git a/fiftyone/operators/types.py b/fiftyone/operators/types.py index f861433eeb..fa5f4e0e9c 100644 --- a/fiftyone/operators/types.py +++ b/fiftyone/operators/types.py @@ -2432,13 +2432,43 @@ def to_json(self): class FrameLoaderView(View): - """Utility for loading frames and animated panels. + """Utility for animating panel state based on the given timeline_name. + + Examples:: + + def on_load(self, ctx): + panel.state.plot = { + "type": "scatter", + "x": [1, 2, 3], + "y": [1, 2, 3], + } + + def render(self, ctx): + panel.obj( + "frame_data", + view=types.FrameLoaderView( + on_load_range=self.on_load_range, + target="plot.selectedpoints", + ), + ) + panel.plot("plot") + + def load_range(self, ctx, range_to_load): + r = ctx.params.get("range") + + chunk = {} + for i in range(r[0], r[1]): + rendered_frame = [i] + chunk[f"frame_data.frames[{i}]"] = rendered_frame + + ctx.panel.set_data(chunk) + current_field = ctx.panel.state.selected_field or "default_field" + ctx.panel.set_state("frame_data.signature", current_field + str(r)) Args: - timeline_id (None): the ID of the timeline to load - on_load (None): the operator to execute when the frame is loaded - on_error (None): the operator to execute when the frame fails to load + timeline_name (None): the name of the timeline to load if provided, otherwise the default timeline on_load_range (None): the operator to execute when the frame is loading + target: the path to the property to animate """ def __init__(self, **kwargs): From cbedc2c9385999aedcd5f514f27c916d2532f987 Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Mon, 21 Oct 2024 14:02:44 -0700 Subject: [PATCH 5/7] fix overlay z-indax for panel menus --- .../plugins/SchemaIO/components/ContainerizedComponent.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/packages/core/src/plugins/SchemaIO/components/ContainerizedComponent.tsx b/app/packages/core/src/plugins/SchemaIO/components/ContainerizedComponent.tsx index 4caadbd7f3..78527693a3 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/ContainerizedComponent.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/ContainerizedComponent.tsx @@ -7,6 +7,7 @@ import { overlayToSx, } from "../utils"; import { ViewPropsType } from "../utils/types"; +import { has } from "lodash"; export default function ContainerizedComponent(props: ContainerizedComponent) { const { schema, children } = props; @@ -22,7 +23,11 @@ export default function ContainerizedComponent(props: ContainerizedComponent) { } if (isCompositeView(schema)) { + const hasOverlay = !!schema?.view?.overlay; const sxForOverlay = overlayToSx[schema?.view?.overlay] || {}; + if (hasOverlay) { + sxForOverlay.zIndex = 999; + } return ( {containerizedChildren} From dcf166b6b2506c7bfdfbcf4d5df51cefcf0a61ab Mon Sep 17 00:00:00 2001 From: Ritchie Martori Date: Tue, 22 Oct 2024 09:45:38 -0700 Subject: [PATCH 6/7] fix panel overflow --- .../plugins/SchemaIO/components/GridView.tsx | 13 ++++----- .../core/src/plugins/SchemaIO/utils/layout.ts | 27 ++++++++++++++++--- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/app/packages/core/src/plugins/SchemaIO/components/GridView.tsx b/app/packages/core/src/plugins/SchemaIO/components/GridView.tsx index 04644549bb..b6dec184d7 100644 --- a/app/packages/core/src/plugins/SchemaIO/components/GridView.tsx +++ b/app/packages/core/src/plugins/SchemaIO/components/GridView.tsx @@ -2,7 +2,7 @@ import { Box, BoxProps } from "@mui/material"; import React from "react"; import { HeaderView } from "."; import { - getAdjustedLayoutWidth, + getAdjustedLayoutDimensions, getComponentProps, getGridSx, getPath, @@ -21,12 +21,13 @@ export default function GridView(props: ViewPropsType) { const propertiesAsArray = Object.entries(properties).map(([id, property]) => { return { id, ...property }; }); - const height = props?.layout?.height as number; const parsedGap = parseGap(gap); - const width = getAdjustedLayoutWidth( - props?.layout?.width, - parsedGap - ) as number; + const { height, width } = getAdjustedLayoutDimensions({ + height: props?.layout?.height, + width: props?.layout?.width, + gap: parsedGap, + orientation, + }); const baseGridProps: BoxProps = { sx: { gap: parsedGap, ...getGridSx(view) }, diff --git a/app/packages/core/src/plugins/SchemaIO/utils/layout.ts b/app/packages/core/src/plugins/SchemaIO/utils/layout.ts index 8f5bfa35be..5801bdce83 100644 --- a/app/packages/core/src/plugins/SchemaIO/utils/layout.ts +++ b/app/packages/core/src/plugins/SchemaIO/utils/layout.ts @@ -179,11 +179,30 @@ export function parseGap(gap: number | string) { return 0; } -export function getAdjustedLayoutWidth(layoutWidth?: number, gap?: number) { - if (typeof gap === "number" && typeof layoutWidth === "number") { - return layoutWidth - gap * 8; +export function getAdjustedLayoutDimension(value?: number, gap?: number) { + if (typeof gap === "number" && typeof value === "number") { + return value - gap * 8; } - return layoutWidth; + return value; +} + +export function getAdjustedLayoutDimensions({ + height, + width, + gap, + orientation, +}: { + height?: number; + width?: number; + gap?: number; + orientation?: string; +}) { + const adjustedHeight = getAdjustedLayoutDimension(height, gap); + const adjustedWidth = getAdjustedLayoutDimension(width, gap); + if (orientation === "horizontal") { + return { height, width: adjustedWidth }; + } + return { height: adjustedHeight, width }; } type PaddingSxType = { From 26431e2cea140a580a9ebd7b0856e729ed2a6c8c Mon Sep 17 00:00:00 2001 From: Minh-Tue Vo Date: Tue, 22 Oct 2024 11:55:01 -0700 Subject: [PATCH 7/7] Toast component refactor (#4957) * Refactored Toast component to support a primary and secondary button and exposed visibility state * Addressed feedback --- .../components/src/components/Toast/Toast.tsx | 32 +++++++++++++------ .../components/src/components/Toast/index.ts | 2 +- .../components/src/components/index.ts | 2 +- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/app/packages/components/src/components/Toast/Toast.tsx b/app/packages/components/src/components/Toast/Toast.tsx index 6cd18102eb..c16c7ec733 100644 --- a/app/packages/components/src/components/Toast/Toast.tsx +++ b/app/packages/components/src/components/Toast/Toast.tsx @@ -1,18 +1,23 @@ -import React, { useState } from "react"; -import { Snackbar, Button, SnackbarContent } from "@mui/material"; -import Box from '@mui/material/Box'; -import Typography from '@mui/material/Typography'; -import BoltIcon from '@mui/icons-material/Bolt'; // Icon for the lightning bolt +import React from "react"; +import { atom, useRecoilState } from "recoil"; +import { Box, Snackbar, SnackbarContent } from "@mui/material"; // Define types for the props interface ToastProps { - action: React.ReactNode; // Accepts any valid React component, element, or JSX - message: React.ReactNode; // Accepts any valid React component, element, or JSX + message: React.ReactNode; + primary: (setOpen: React.Dispatch>) => React.ReactNode; + secondary: (setOpen: React.Dispatch>) => React.ReactNode; duration?: number; // Optional duration, with a default value } -const Toast: React.FC = ({ action, message, duration = 5000 }) => { - const [open, setOpen] = useState(true); +const toastStateAtom = atom({ + key: "toastOpenState", + default: true, +}); + +const Toast: React.FC = ({message, primary, secondary, duration = 5000 }) => { + + const [open, setOpen] = useRecoilState(toastStateAtom); // State management for toast visibility const handleClose = (event, reason) => { if (reason === "clickaway") { @@ -21,6 +26,15 @@ const Toast: React.FC = ({ action, message, duration = 5000 }) => { setOpen(false); }; + const action = ( +
+ + {primary(setOpen)} {/* Pass setOpen to primary button */} + {secondary(setOpen)} {/* Pass setOpen to secondary button */} + +
+ ); + return (