Skip to content

Commit

Permalink
Merge pull request #5141 from voxel51/merge/release/v1.1.0
Browse files Browse the repository at this point in the history
Merge `release/v1.1.0` to `develop`
  • Loading branch information
benjaminpkane authored Nov 19, 2024
2 parents 6125f1d + 2949c29 commit 15e7ece
Show file tree
Hide file tree
Showing 22 changed files with 322 additions and 425 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ const BoxDiv = styled.div`
export default function Box({
children,
text,
}: React.PropsWithChildren<{ text?: string }>) {
height = 30,
}: React.PropsWithChildren<{ text?: string; height?: number }>) {
const value = text === "Loading" ? <LoadingDots text="Loading" /> : text;
return (
<BoxDiv
style={{
height: 71,
height: height,
display: "flex",
justifyContent: "center",
flexDirection: "column",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function ActionsMenu(props: ActionsPropsType) {

if (actions.length <= maxInline) {
return (
<Stack direction="row" spacing={0.5} justifyContent="flex-start">
<Stack direction="row" spacing={0.5} justifyContent="flex-end">
{actions.map((action) => (
<Action {...action} key={action.name} mode="inline" size={size} />
))}
Expand Down Expand Up @@ -95,7 +95,11 @@ function Action(props: ActionPropsType) {
variant={variant}
startIcon={Icon}
onClick={handleClick}
sx={{ color: resolvedColor, padding: size === "small" ? 0 : undefined }}
sx={{
color: resolvedColor,
padding: size === "small" ? 0 : undefined,
minWidth: size === "small" ? 40 : undefined,
}}
>
{label}
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import React from "react";
import { MuiIconFont } from "@fiftyone/components";
import { OperatorExecutionButton } from "@fiftyone/operators";
import { OperatorExecutionButton, usePanelEvent } from "@fiftyone/operators";
import { usePanelId } from "@fiftyone/spaces";
import { isNullish } from "@fiftyone/utilities";
import { Box, ButtonProps, Typography } from "@mui/material";
import { getColorByCode, getComponentProps, getDisabledColors } from "../utils";
import { ViewPropsType } from "../utils/types";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import TooltipProvider from "./TooltipProvider";
import { OperatorExecutionOption } from "@fiftyone/operators/src/state";
import {
ExecutionCallback,
ExecutionErrorCallback,
} from "@fiftyone/operators/src/types-internal";
import { OperatorResult } from "@fiftyone/operators/src/operators";

export default function OperatorExecutionButtonView(props: ViewPropsType) {
const { schema, path } = props;
Expand All @@ -21,6 +27,9 @@ export default function OperatorExecutionButtonView(props: ViewPropsType) {
params = {},
title,
disabled = false,
on_error,
on_success,
on_option_selected,
} = view;
const panelId = usePanelId();
const variant = getVariant(props);
Expand All @@ -35,11 +44,56 @@ export default function OperatorExecutionButtonView(props: ViewPropsType) {
<ExpandMoreIcon />
);

const triggerEvent = usePanelEvent();

const handleOnSuccess: ExecutionCallback = (
operatorResult: OperatorResult,
{ ctx }
) => {
if (on_success) {
triggerEvent(panelId, {
operator: on_success,
params: {
result: operatorResult.result,
original_params: ctx.params,
},
});
}
};
const handleOnError: ExecutionErrorCallback = (
result: OperatorResult,
{ ctx }
) => {
if (on_error) {
triggerEvent(panelId, {
operator: on_error,
params: {
error: result.error,
error_message: result.errorMessage,
original_params: ctx.params,
},
});
}
};
const handleOnOptionSelected = (option: OperatorExecutionOption) => {
if (on_option_selected) {
triggerEvent(panelId, {
operator: on_option_selected,
params: {
selected_option: option,
},
});
}
};

return (
<Box {...getComponentProps(props, "container")}>
<TooltipProvider title={title} {...getComponentProps(props, "tooltip")}>
<OperatorExecutionButton
operatorUri={operator}
onSuccess={handleOnSuccess}
onError={handleOnError}
onOptionSelected={handleOnOptionSelected}
executionParams={computedParams}
variant={variant}
disabled={disabled}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export default function TableView(props: ViewPropsType) {
{hasRowActions && (
<TableCell
{...getComponentProps(props, "tableHeadCell", {
sx: { ...headingCellBaseStyles, textAlign: "left" },
sx: { ...headingCellBaseStyles, textAlign: "center" },
})}
width={`${max_inline_actions * 5}%`}
onClick={() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const OperatorExecutionButton = ({
operatorUri,
onSuccess,
onError,
onClick,
executionParams,
onOptionSelected,
disabled,
Expand All @@ -30,6 +31,7 @@ export const OperatorExecutionButton = ({
operatorUri: string;
onSuccess?: ExecutionCallback;
onError?: ExecutionErrorCallback;
onClick?: () => void;
executionParams?: object;
onOptionSelected?: (option: OperatorExecutionOption) => void;
disabled?: boolean;
Expand All @@ -38,6 +40,7 @@ export const OperatorExecutionButton = ({
return (
<OperatorExecutionTrigger
operatorUri={operatorUri}
onClick={onClick}
onSuccess={onSuccess}
onError={onError}
executionParams={executionParams}
Expand Down
23 changes: 10 additions & 13 deletions app/packages/operators/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1012,21 +1012,18 @@ export function useOperatorExecutor(uri, handlers: any = {}) {
setResult(result.result);
setError(result.error);
setIsDelegated(result.delegated);
handlers.onSuccess?.(result, { ctx });
callback?.(result, { ctx });
if (result.error) {
const isAbortError =
result.error.name === "AbortError" ||
result.error instanceof DOMException;
if (!isAbortError) {
notify({
msg: result.errorMessage || `Operation failed: ${uri}`,
variant: "error",
});
console.error("Error executing operator", uri, result.errorMessage);
console.error(result.error);
}
handlers.onError?.(result, { ctx });
notify({
msg: result.errorMessage || `Operation failed: ${uri}`,
variant: "error",
});
console.error("Error executing operator", uri, result.errorMessage);
console.error(result.error);
} else {
handlers.onSuccess?.(result, { ctx });
}
callback?.(result, { ctx });
} catch (e) {
callback?.(new OperatorResult(operator, null, ctx.executor, e, false), {
ctx,
Expand Down
12 changes: 9 additions & 3 deletions app/packages/operators/src/types-internal.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { ExecutionContext, OperatorResult } from "./operators";

export type ExecutionCallbackOptions = {ctx: ExecutionContext};
export type ExecutionCallback = (result: OperatorResult, options: ExecutionCallbackOptions) => void;
export type ExecutionErrorCallback = (error: Error) => void;
export type ExecutionCallbackOptions = { ctx: ExecutionContext };
export type ExecutionCallback = (
result: OperatorResult,
options: ExecutionCallbackOptions
) => void;
export type ExecutionErrorCallback = (
error: OperatorResult,
options: ExecutionCallbackOptions
) => void;

export type OperatorExecutorOptions = {
delegationTarget?: string;
Expand Down
7 changes: 5 additions & 2 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -374,8 +374,11 @@ customizing the functionality of the tool to suit your specific needs.

With plugins, you can add new functionality to the FiftyOne App, create
integrations with other tools and APIs, render custom panels, and add custom
buttons to menus. You can even schedule long running tasks from within the App
that execute on a connected workflow orchestration tool like Apache Airflow.
buttons to menus.

With :ref:`FiftyOne Teams <teams-delegated-operations>`, you can even write
plugins that allow users to execute long-running tasks from within the App that
run on a connected compute cluster.

.. custombutton::
:button_text: Install some plugins!
Expand Down
103 changes: 93 additions & 10 deletions docs/source/plugins/developing_plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1128,19 +1128,17 @@ computation, evaluation, and exports are computationally intensive and/or not
suitable for immediate execution.

In such cases, :ref:`delegated operations <delegated-operations>` come to the
rescue by allowing operators to schedule tasks that are executed on a connected
workflow orchestrator like :ref:`Apache Airflow <delegated-operations-airflow>`
or run just :ref:`run locally <delegated-operations-local>` in a separate
process.
rescue by allowing users to schedule potentially long-running tasks that are
executed in the background while you continue to use the App.

.. note::

Even though delegated operations are run in a separate process or physical
location, they are provided with the same `ctx` that was hydrated by the
operator's :ref:`input form <operator-inputs>`.
:ref:`FiftyOne Teams <teams-delegated-operations>` deployments come out of
the box with a connected compute cluster for executing delegated operations
at scale.

Refer to :ref:`this section <delegated-operations>` for more information
about how delegated operations are executed.
In FiftyOne Open Source, you can use delegated operations at small scale
by :ref:`running them locally <delegated-orchestrator-open-source>`.

There are a variety of options available for configuring whether a given
operation should be delegated or executed immediately.
Expand Down Expand Up @@ -2430,6 +2428,91 @@ loaded only when the `brain_key` property is modified.
Panel data is never readable in Python; it is only implicitly used by
the types you define when they are rendered clientside.

.. _panel-execution-store
Execution store
---------------

Panels can store data in the execution store, which is a key-value store that
is persisted beyond the lifetime of the panel. This is useful for storing
information that should persist across panel instances and App sessions, such
as cached data, long-lived panel state, or user preferences.

You can create/retrieve execution stores scoped to the current ``ctx.dataset``
via :meth:`ctx.store <fiftyone.operators.executor.ExecutionContext.store>`:

.. code-block:: python
:linenos:
def on_load(ctx):
# Retrieve a store scoped to the current `ctx.dataset`
# The store is automatically created if necessary
store = ctx.store("my_store")
# Load a pre-existing value from the store
user_choice = store.get("user_choice")
# Store data with a TTL to ensure it is evicted after `ttl` seconds
store.set("my_key", {"foo": "bar"}, ttl=60)
# List all keys in the store
print(store.list_keys()) # ["user_choice", "my_key"]
# Retrieve data from the store
print(store.get("my_key")) # {"foo": "bar"}
# Retrieve metadata about a key
print(store.get_metadata("my_key"))
# {"created_at": ..., "updated_at": ..., "expires_at": ...}
# Delete a key from the store
store.delete("my_key")
# Clear all data in the store
store.clear()
.. note::

Did you know? Any execution stores associated with a dataset are
automatically deleted when the dataset is deleted.

For advanced use cases, it is also possible to create and use global stores
that are available to all datasets via the
:class:`ExecutionStore <fiftyone.operators.store.ExecutionStore>` class:

.. code-block:: python
:linenos:
from fiftyone.operators import ExecutionStore
# Retrieve a global store
# The store is automatically created if necessary
store = ExecutionStore.create("my_store")
# Store data with a TTL to ensure it is evicted after `ttl` seconds
store.set("my_key", {"foo": "bar"}, ttl=60)
# List all keys in the global store
print(store.list_keys()) # ["my_key"]
# Retrieve data from the global store
print(store.get("my_key")) # {"foo": "bar"}
# Retrieve metadata about a key
print(store.get_metadata("my_key"))
# {"created_at": ..., "updated_at": ..., "expires_at": ...}
# Delete a key from the global store
store.delete("my_key")
# Clear all data in the global store
store.clear()
.. warning::

Global stores have no automatic garbage collection, so take care when
creating and using global stores whose keys do not utilize TTLs.

.. _panel-saved-workspaces
Saved workspaces
Expand Down Expand Up @@ -3566,7 +3649,7 @@ Delegated execution
~~~~~~~~~~~~~~~~~~~

Python operations may also be :ref:`delegated <operator-delegated-execution>`
to an external orchestrator like Apache Airflow or a local process.
for execution in the background.

When an operation is delegated, the following happens:

Expand Down
7 changes: 5 additions & 2 deletions docs/source/plugins/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ customizing the functionality of the tool to suit your specific needs.

With plugins, you can add new functionality to the FiftyOne App, create
integrations with other tools and APIs, render custom panels, and add custom
actions to menus. You can even schedule long running tasks from within the App
that execute on a connected workflow orchestration tool like Apache Airflow.
actions to menus.

With :ref:`FiftyOne Teams <teams-delegated-operations>`, you can even write
plugins that allow users to execute long-running tasks from within the App that
run on a connected compute cluster.

Get started with plugins by installing some
:ref:`popular plugins <plugins-getting-started>`, then try your hand at
Expand Down
Loading

0 comments on commit 15e7ece

Please sign in to comment.