From 456fef72e64d0d60527efa72497994ad08261dcb Mon Sep 17 00:00:00 2001 From: Mikyo King Date: Thu, 6 Apr 2023 09:05:32 -0600 Subject: [PATCH] fix: display point-cloud errors in the UI for troubleshooting (#525) * v0.0.9 * fix: show point-cloud errors in the UI --- app/src/App.tsx | 33 +++++---- app/src/contexts/NotificationContext.tsx | 86 ++++++++++++++++++++++++ app/src/contexts/index.tsx | 1 + app/src/pages/embedding/Embedding.tsx | 32 ++++++++- app/src/store/pointCloudStore.ts | 22 +++++- src/phoenix/__about__.py | 2 +- 6 files changed, 158 insertions(+), 18 deletions(-) create mode 100644 app/src/contexts/NotificationContext.tsx diff --git a/app/src/App.tsx b/app/src/App.tsx index 9eb2460f93..a0bf36f92a 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -11,8 +11,11 @@ import { ThemeProvider } from "@emotion/react"; import { Provider, theme } from "@arizeai/components"; import { AppRootQuery } from "./__generated__/AppRootQuery.graphql"; -import { TimeRangeProvider } from "./contexts/TimeRangeContext"; -import { DatasetsProvider } from "./contexts"; +import { + DatasetsProvider, + NotificationProvider, + TimeRangeProvider, +} from "./contexts"; import { GlobalStyles } from "./GlobalStyles"; import RelayEnvironment from "./RelayEnvironment"; import { AppRoutes } from "./Routes"; @@ -46,19 +49,21 @@ function App(props: AppProps) { } = usePreloadedQuery(RootQuery, props.preloadedQuery); return ( - - + - - - + + + + + ); } diff --git a/app/src/contexts/NotificationContext.tsx b/app/src/contexts/NotificationContext.tsx new file mode 100644 index 0000000000..e84b458805 --- /dev/null +++ b/app/src/contexts/NotificationContext.tsx @@ -0,0 +1,86 @@ +import React, { createContext, useCallback, useContext } from "react"; + +import { NoticeFn, useNotification } from "@arizeai/components"; + +// Extract the first argument of notify +type NoticeConfig = Parameters[0]; +type NoticeConfigWithoutVariant = Omit; +type NotificationContextType = { + /** + * Send a notification that is visible in any part of the UI + */ + notify: NoticeFn; + /** + * Convenience function to notify of an error + */ + notifyError: (notice: NoticeConfigWithoutVariant) => void; + /** + * Convenience function to notify of a success + */ + notifySuccess: (notice: NoticeConfigWithoutVariant) => void; +}; + +const NotificationContext = createContext(null); + +export function NotificationProvider({ + children, +}: { + children: React.ReactNode; +}) { + const [notify, holder] = useNotification(); + + const notifyError = useCallback( + (notice: NoticeConfigWithoutVariant) => { + notify({ + variant: "danger", + ...notice, + }); + }, + [notify] + ); + + const notifySuccess = useCallback( + (notice: NoticeConfigWithoutVariant) => { + notify({ + variant: "success", + ...notice, + }); + }, + [notify] + ); + + return ( + + {children} + {holder} + + ); +} + +export function useGlobalNotification() { + const context = useContext(NotificationContext); + if (context === null) { + throw new Error( + "useGlobalNotification must be used within a NotificationProvider" + ); + } + return context; +} + +/** + * Convenience hook to display an error at the global app level + */ +export function useNotifyError() { + const context = useGlobalNotification(); + return context.notifyError; +} + +/** + * Convenience hook to display a success at the global app level + */ +export function useNotifySuccess() { + const context = useGlobalNotification(); + return context.notifySuccess; +} diff --git a/app/src/contexts/index.tsx b/app/src/contexts/index.tsx index cc0152a7e7..c916479cd7 100644 --- a/app/src/contexts/index.tsx +++ b/app/src/contexts/index.tsx @@ -1,3 +1,4 @@ export * from "./DatasetsContext"; export * from "./PointCloudContext"; export * from "./TimeRangeContext"; +export * from "./NotificationContext"; diff --git a/app/src/pages/embedding/Embedding.tsx b/app/src/pages/embedding/Embedding.tsx index d29f39cc76..e018f984c3 100644 --- a/app/src/pages/embedding/Embedding.tsx +++ b/app/src/pages/embedding/Embedding.tsx @@ -35,7 +35,11 @@ import { compactResizeHandleCSS, resizeHandleCSS, } from "@phoenix/components/resize/styles"; -import { PointCloudProvider, usePointCloudContext } from "@phoenix/contexts"; +import { + PointCloudProvider, + useGlobalNotification, + usePointCloudContext, +} from "@phoenix/contexts"; import { useDatasets } from "@phoenix/contexts"; import { useTimeRange } from "@phoenix/contexts/TimeRangeContext"; import { @@ -224,6 +228,7 @@ function EmbeddingMain() { background-color: ${theme.colors.gray900}; `} > + ); } + +function PointCloudNotifications() { + const { notifyError } = useGlobalNotification(); + const errorMessage = usePointCloudContext((state) => state.errorMessage); + const setErrorMessage = usePointCloudContext( + (state) => state.setErrorMessage + ); + + useEffect(() => { + if (errorMessage !== null) { + notifyError({ + title: "An error occurred", + message: errorMessage, + action: { + text: "Dismiss", + onClick: () => { + setErrorMessage(null); + }, + }, + }); + } + }, [errorMessage, notifyError, setErrorMessage]); + + return null; +} diff --git a/app/src/store/pointCloudStore.ts b/app/src/store/pointCloudStore.ts index d0aa35690c..f52f4e67f5 100644 --- a/app/src/store/pointCloudStore.ts +++ b/app/src/store/pointCloudStore.ts @@ -207,6 +207,10 @@ export interface PointCloudProps { * The clustering / HDBSCAN parameters */ hdbscanParameters: HDBSCANParameters; + /** + * An error message if anything occurs during point-cloud data loads + */ + errorMessage: string | null; } export interface PointCloudState extends PointCloudProps { @@ -273,6 +277,10 @@ export interface PointCloudState extends PointCloudProps { * Done when the point cloud is re-loaded */ reset: () => void; + /** + * Set the error message + */ + setErrorMessage: (message: string | null) => void; } /** @@ -311,6 +319,7 @@ export type PointCloudStore = ReturnType; export const createPointCloudStore = (initProps?: Partial) => { // The default props irrespective of the number of datasets const defaultProps: PointCloudProps = { + errorMessage: null, points: [], pointData: null, selectedEventIds: new Set(), @@ -365,7 +374,11 @@ export const createPointCloudStore = (initProps?: Partial) => { }); // Re-compute the point coloring once the granular data is loaded - const pointData = await fetchPointEvents(points.map((p) => p.eventId)); + const pointData = await fetchPointEvents( + points.map((p) => p.eventId) + ).catch(() => set({ errorMessage: "Failed to load the point events" })); + + if (!pointData) return; // The error occurred above set({ pointData, @@ -481,7 +494,11 @@ export const createPointCloudStore = (initProps?: Partial) => { setDimension: async (dimension) => { const pointCloudState = get(); set({ dimension, dimensionMetadata: null }); - const dimensionMetadata = await fetchDimensionMetadata(dimension); + const dimensionMetadata = await fetchDimensionMetadata(dimension).catch( + () => set({ errorMessage: "Failed to load the dimension metadata" }) + ); + if (!dimensionMetadata) return; // The error occurred above + set({ dimensionMetadata }); if (dimensionMetadata.categories && dimensionMetadata.categories.length) { const numCategories = dimensionMetadata.categories.length; @@ -558,6 +575,7 @@ export const createPointCloudStore = (initProps?: Partial) => { setDimensionMetadata: (dimensionMetadata) => set({ dimensionMetadata }), setUMAPParameters: (umapParameters) => set({ umapParameters }), setHDBSCANParameters: (hdbscanParameters) => set({ hdbscanParameters }), + setErrorMessage: (errorMessage) => set({ errorMessage }), }); return create()(devtools(pointCloudStore)); diff --git a/src/phoenix/__about__.py b/src/phoenix/__about__.py index a73339bf81..00ec2dcdb2 100644 --- a/src/phoenix/__about__.py +++ b/src/phoenix/__about__.py @@ -1 +1 @@ -__version__ = "0.0.8" +__version__ = "0.0.9"