From e5a2dc12cae3404eede8fe7f176f4dbc1071cefb Mon Sep 17 00:00:00 2001 From: imanjra Date: Mon, 7 Oct 2024 00:15:45 -0400 Subject: [PATCH] add spaces context to operators --- app/packages/operators/src/CustomPanel.tsx | 4 +++ 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 | 27 +++++++++++++++++++ fiftyone/operators/builtin.py | 13 ++++----- fiftyone/operators/executor.py | 12 +++++++++ fiftyone/operators/operations.py | 2 ++ fiftyone/operators/panel.py | 2 ++ fiftyone/server/decorators.py | 3 ++- 11 files changed, 101 insertions(+), 8 deletions(-) diff --git a/app/packages/operators/src/CustomPanel.tsx b/app/packages/operators/src/CustomPanel.tsx index cf9f82a5b32..10891a5c546 100644 --- a/app/packages/operators/src/CustomPanel.tsx +++ b/app/packages/operators/src/CustomPanel.tsx @@ -115,6 +115,8 @@ export function defineCustomPanel({ on_change_selected_labels, on_change_extended_selection, on_change_group_slice, + on_change_spaces, + on_change_workspace, panel_name, panel_label, }) { @@ -132,6 +134,8 @@ export function defineCustomPanel({ onChangeSelectedLabels={on_change_selected_labels} onChangeExtendedSelection={on_change_extended_selection} onChangeGroupSlice={on_change_group_slice} + onChangeSpaces={on_change_spaces} + onChangeWorkspace={on_change_workspace} 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 a636f1b2639..855c6a2b1c8 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 d16f175fba6..a96f5054065 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 cf0f08cd149..535f61c0c2e 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 afbaf851dd1..155f67b98c0 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 68b9a1c12a9..434a27e6cd6 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,32 @@ 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) + + def on_change_workspace(self, ctx): + """Implement this method to set panel state/data when the current + workspace changes in the app. + + The current workspace will be available via ``ctx.workspace``. + """ + event = { + "data": ctx.workspace, + "description": "the current workspace", + } + ctx.panel.set_state("event", "on_change_workspace") + 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 12d4ee76bbb..653b1015f79 100644 --- a/fiftyone/operators/builtin.py +++ b/fiftyone/operators/builtin.py @@ -1939,18 +1939,18 @@ def resolve_input(self, ctx): ), ) - # @todo infer this automatically from current App spaces + current_spaces = ctx.spaces.to_json(True) if ctx.spaces else None spaces_prop = inputs.oneof( "spaces", [types.String(), types.Object()], - default=None, + default=current_spaces, required=True, label="Spaces", description=( "JSON description of the workspace to save: " "`print(session.spaces.to_json(True))`" ), - view=types.CodeView(), + view=types.CodeView(language="json"), ) spaces = ctx.params.get("spaces", None) @@ -2034,11 +2034,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 +2103,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 e0e6ac784c9..7624653a14e 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 933f6f3d55e..50f0a2c4cb6 100644 --- a/fiftyone/operators/operations.py +++ b/fiftyone/operators/operations.py @@ -356,6 +356,8 @@ 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 + on_change_workspace (None): an operator to invoke when the current workspace 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 b9d897c2fdb..7ade070053d 100644 --- a/fiftyone/operators/panel.py +++ b/fiftyone/operators/panel.py @@ -126,6 +126,8 @@ def on_startup(self, ctx): "on_change_selected_labels", "on_change_extended_selection", "on_change_group_slice", + "on_change_spaces", + "on_change_workspace", ] for method in methods + ctx_change_events: if hasattr(self, method) and callable(getattr(self, method)): diff --git a/fiftyone/server/decorators.py b/fiftyone/server/decorators.py index 6f013b28944..c6f1a42351d 100644 --- a/fiftyone/server/decorators.py +++ b/fiftyone/server/decorators.py @@ -34,7 +34,8 @@ def default(self, o): async def create_response(response: dict): return Response( - await run_sync_task(lambda: json_util.dumps(response, cls=Encoder)) + await run_sync_task(lambda: json_util.dumps(response, cls=Encoder)), + media_type="application/json", )