Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: display point-cloud errors in the UI for troubleshooting #525

Merged
merged 2 commits into from
Apr 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -46,19 +49,21 @@ function App(props: AppProps) {
} = usePreloadedQuery(RootQuery, props.preloadedQuery);

return (
<DatasetsProvider
primaryDataset={primaryDataset}
referenceDataset={referenceDataset ?? null}
>
<TimeRangeProvider
timeRangeBounds={{
start: new Date(primaryDataset.startTime),
end: new Date(primaryDataset.endTime),
}}
<NotificationProvider>
<DatasetsProvider
primaryDataset={primaryDataset}
referenceDataset={referenceDataset ?? null}
>
<AppRoutes />
</TimeRangeProvider>
</DatasetsProvider>
<TimeRangeProvider
timeRangeBounds={{
start: new Date(primaryDataset.startTime),
end: new Date(primaryDataset.endTime),
}}
>
<AppRoutes />
</TimeRangeProvider>
</DatasetsProvider>
</NotificationProvider>
);
}

Expand Down
86 changes: 86 additions & 0 deletions app/src/contexts/NotificationContext.tsx
Original file line number Diff line number Diff line change
@@ -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<NoticeFn>[0];
type NoticeConfigWithoutVariant = Omit<NoticeConfig, "variant">;
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<NotificationContextType | null>(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 (
<NotificationContext.Provider
value={{ notify, notifyError, notifySuccess }}
>
{children}
{holder}
</NotificationContext.Provider>
);
}

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;
}
1 change: 1 addition & 0 deletions app/src/contexts/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./DatasetsContext";
export * from "./PointCloudContext";
export * from "./TimeRangeContext";
export * from "./NotificationContext";
32 changes: 31 additions & 1 deletion app/src/pages/embedding/Embedding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -224,6 +228,7 @@ function EmbeddingMain() {
background-color: ${theme.colors.gray900};
`}
>
<PointCloudNotifications />
<Toolbar
extra={
<Switch
Expand Down Expand Up @@ -584,3 +589,28 @@ function ClustersPanelContents({
</Tabs>
);
}

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;
}
22 changes: 20 additions & 2 deletions app/src/store/pointCloudStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -311,6 +319,7 @@ export type PointCloudStore = ReturnType<typeof createPointCloudStore>;
export const createPointCloudStore = (initProps?: Partial<PointCloudProps>) => {
// The default props irrespective of the number of datasets
const defaultProps: PointCloudProps = {
errorMessage: null,
points: [],
pointData: null,
selectedEventIds: new Set(),
Expand Down Expand Up @@ -365,7 +374,11 @@ export const createPointCloudStore = (initProps?: Partial<PointCloudProps>) => {
});

// 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,
Expand Down Expand Up @@ -481,7 +494,11 @@ export const createPointCloudStore = (initProps?: Partial<PointCloudProps>) => {
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;
Expand Down Expand Up @@ -558,6 +575,7 @@ export const createPointCloudStore = (initProps?: Partial<PointCloudProps>) => {
setDimensionMetadata: (dimensionMetadata) => set({ dimensionMetadata }),
setUMAPParameters: (umapParameters) => set({ umapParameters }),
setHDBSCANParameters: (hdbscanParameters) => set({ hdbscanParameters }),
setErrorMessage: (errorMessage) => set({ errorMessage }),
});

return create<PointCloudState>()(devtools(pointCloudStore));
Expand Down
2 changes: 1 addition & 1 deletion src/phoenix/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.0.8"
__version__ = "0.0.9"