Skip to content

Commit

Permalink
add panel loading state indicator support
Browse files Browse the repository at this point in the history
  • Loading branch information
imanjra committed Aug 13, 2024
1 parent da67848 commit e65e8a6
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -302,10 +302,13 @@ export default function DashboardView(props: ViewPropsType) {
const handleAutoLayoutChange = (event) => {
const { checked } = event.target;
setAutoLayout(checked);
triggerPanelEvent(panelId, {
operator: schema.view.on_auto_layout_change,
params: { auto_layout: checked },
});
const operator = schema.view.on_auto_layout_change;
if (operator) {
triggerPanelEvent(panelId, {
operator,
params: { auto_layout: checked },
});
}
};

const handleNumRowsChange = (event) => {
Expand Down
17 changes: 14 additions & 3 deletions app/packages/operators/src/CustomPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { CenteredStack, CodeBlock } from "@fiftyone/components";
import { PanelSkeleton, useSetPanelCloseEffect } from "@fiftyone/spaces";
import { clearUseKeyStores } from "@fiftyone/core/src/plugins/SchemaIO/hooks";
import {
PanelSkeleton,
usePanelLoading,
useSetPanelCloseEffect,
} from "@fiftyone/spaces";
import * as fos from "@fiftyone/state";
import { Box, Typography } from "@mui/material";
import { useEffect } from "react";
import OperatorIO from "./OperatorIO";
import { PANEL_LOAD_TIMEOUT } from "./constants";
import { useActivePanelEventsCount } from "./hooks";
import { Property } from "./types";
import { CustomPanelProps, useCustomPanelHooks } from "./useCustomPanelHooks";
import { useEffect } from "react";
import { clearUseKeyStores } from "@fiftyone/core/src/plugins/SchemaIO/hooks";

export function CustomPanel(props: CustomPanelProps) {
const { panelId, dimensions, panelName, panelLabel } = props;
const { height, width } = dimensions?.bounds || {};
const { count } = useActivePanelEventsCount(panelId);
const [_, setLoading] = usePanelLoading(panelId);

const {
handlePanelStateChange,
Expand All @@ -29,6 +36,10 @@ export function CustomPanel(props: CustomPanelProps) {
});
}, []);

useEffect(() => {
setLoading(count > 0);
}, [setLoading, count]);

if (pending && !panelSchema && !onLoadError) {
return <PanelSkeleton />;
}
Expand Down
41 changes: 39 additions & 2 deletions app/packages/operators/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { pluginsLoaderAtom } from "@fiftyone/plugins";
import * as fos from "@fiftyone/state";
import { debounce, isEqual } from "lodash";
import { useEffect, useMemo, useRef, useState } from "react";
import { useRecoilValue, useSetRecoilState } from "recoil";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useRecoilValue, useSetRecoilState, useRecoilState } from "recoil";
import { RESOLVE_PLACEMENTS_TTL } from "./constants";
import {
ExecutionContext,
fetchRemotePlacements,
resolveLocalPlacements,
} from "./operators";
import {
activePanelsEventCountAtom,
operatorPlacementsAtom,
operatorThrottledContext,
operatorsInitializedAtom,
Expand Down Expand Up @@ -103,3 +104,39 @@ export function useOperatorPlacementsResolver() {

return { resolving, initialized };
}

export function useActivePanelEventsCount(id: string) {
const [activePanelEventsCount, setActivePanelEventsCount] = useRecoilState(
activePanelsEventCountAtom
);
const count = useMemo(() => {
return activePanelEventsCount.get(id) || 0;
}, [activePanelEventsCount, id]);

const increment = useCallback(
(panelId?: string) => {
const computedId = panelId ?? id;
setActivePanelEventsCount((counts) => {
const updatedCount = (counts.get(computedId) || 0) + 1;
return new Map(counts).set(computedId, updatedCount);
});
},
[id, setActivePanelEventsCount]
);

const decrement = useCallback(
(panelId?: string) => {
const computedId = panelId ?? id;
setActivePanelEventsCount((counts) => {
const updatedCount = (counts.get(computedId) || 0) - 1;
if (updatedCount < 0) {
return counts;
}
return new Map(counts).set(computedId, updatedCount);
});
},
[id, setActivePanelEventsCount]
);

return { count, increment, decrement };
}
5 changes: 5 additions & 0 deletions app/packages/operators/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1044,3 +1044,8 @@ export const panelsStateUpdatesCountAtom = atom({
key: "panelsStateUpdatesCountAtom",
default: 0,
});

export const activePanelsEventCountAtom = atom({
key: "activePanelsEventCountAtom",
default: new Map<string, number>(),
});
8 changes: 6 additions & 2 deletions app/packages/operators/src/usePanelEvent.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { usePanelStateByIdCallback } from "@fiftyone/spaces";
import { useNotification } from "@fiftyone/state";
import { useActivePanelEventsCount } from "./hooks";
import { executeOperator } from "./operators";
import { usePromptOperatorInput } from "./state";
import { ExecutionCallback } from "./types-internal";
import { useNotification } from "@fiftyone/state";

type HandlerOptions = {
params: any;
params: { [name: string]: unknown };
operator: string;
prompt?: boolean;
panelId: string;
Expand All @@ -16,6 +17,7 @@ type HandlerOptions = {
export default function usePanelEvent() {
const promptForOperator = usePromptOperatorInput();
const notify = useNotification();
const { increment, decrement } = useActivePanelEventsCount("");
return usePanelStateByIdCallback((panelId, panelState, args) => {
const options = args[0] as HandlerOptions;
const { params, operator, prompt, currentPanelState } = options;
Expand All @@ -27,6 +29,7 @@ export default function usePanelEvent() {
};

const eventCallback = (result) => {
decrement(panelId);
const msg =
result.errorMessage || result.error || "Failed to execute operation";
const computedMsg = `${msg} (operation: ${operator})`;
Expand All @@ -40,6 +43,7 @@ export default function usePanelEvent() {
if (prompt) {
promptForOperator(operator, actualParams, { callback: eventCallback });
} else {
increment(panelId);
executeOperator(operator, actualParams, { callback: eventCallback });
}
});
Expand Down
7 changes: 5 additions & 2 deletions app/packages/spaces/src/components/PanelTab.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { IconButton } from "@fiftyone/components";
import { useTimeout } from "@fiftyone/state";
import { Close } from "@mui/icons-material";
import { Skeleton, Typography } from "@mui/material";
import { CircularProgress, Skeleton, Typography } from "@mui/material";
import { useCallback } from "react";
import { PANEL_LOADING_TIMEOUT } from "../constants";
import {
usePanelCloseEffect,
usePanelLoading,
usePanelTitle,
useReactivePanel,
useSpaces,
Expand All @@ -20,6 +21,7 @@ export default function PanelTab({ node, active, spaceId }: PanelTabProps) {
const panelId = node.id;
const panel = useReactivePanel(panelName);
const [title] = usePanelTitle(panelId);
const [loading] = usePanelLoading(panelId);
const closeEffect = usePanelCloseEffect(panelId);
const pending = useTimeout(PANEL_LOADING_TIMEOUT);

Expand All @@ -46,7 +48,8 @@ export default function PanelTab({ node, active, spaceId }: PanelTabProps) {
>
{!panel && pending && <Skeleton width={48} height={24} />}
{!panel && !pending && <Typography>{panelName}</Typography>}
{panel && <PanelIcon name={panelName as string} />}
{panel && loading && <CircularProgress size={14} sx={{ mr: 0.85 }} />}
{panel && !loading && <PanelIcon name={panelName as string} />}
{panel && <Typography>{title || panel.label || panel.name}</Typography>}
{panel && TabIndicator && (
<TabIndicatorContainer>
Expand Down
31 changes: 31 additions & 0 deletions app/packages/spaces/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
panelStateSelector,
panelTitlesState,
panelsCloseEffect,
panelsLoadingStateAtom,
panelsStateAtom,
previousTabsGroupAtom,
spaceSelector,
Expand Down Expand Up @@ -145,6 +146,36 @@ export function usePanelTitle(id?: string): [string, (title: string) => void] {
return [panelTitle, setPanelTitle];
}

/**
* Get and set loading state of a panel
*
* Note: `id` is optional if hook is used within the component of a panel.
*/
export function usePanelLoading(
id?: string
): [boolean, (loading: boolean, id?: string) => void] {
const panelContext = useContext(PanelContext);
const [panelsLoadingState, setPanelsLoadingState] = useRecoilState(
panelsLoadingStateAtom
);

const panelId = (id || panelContext?.node?.id) as string;
const panelLoading = Boolean(panelsLoadingState.get(panelId));

const setPanelLoading = useCallback(
(loading: boolean, id?: string) => {
setPanelsLoadingState((panelsLoading) => {
const updatedPanelsLoading = new Map(panelsLoading);
updatedPanelsLoading.set(id || panelId, loading);
return updatedPanelsLoading;
});
},
[panelId, setPanelsLoadingState]
);

return [panelLoading, setPanelLoading];
}

export function usePanelContext() {
return useContext(PanelContext);
}
Expand Down
5 changes: 5 additions & 0 deletions app/packages/spaces/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ export const panelTitlesState = atom({
default: new Map(),
});

export const panelsLoadingStateAtom = atom({
key: "panelsLoadingStateAtom",
default: new Map<string, boolean>(),
});

export const panelsStateAtom = atom({
key: "panelsState",
default: new Map(),
Expand Down

0 comments on commit e65e8a6

Please sign in to comment.