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"