feat(ui): custom error toast support (#8001)

* support for custom error toast components, starting with usage limit

* add support for all usage limits

---------

Co-authored-by: Mary Hipp <maryhipp@Marys-MacBook-Air.local>
This commit is contained in:
Mary Hipp Rogers
2025-05-08 15:53:10 -04:00
committed by GitHub
parent 821889148a
commit 954fce3c67
4 changed files with 59 additions and 8 deletions

View File

@@ -18,6 +18,7 @@ import { $openAPISchemaUrl } from 'app/store/nanostores/openAPISchemaUrl';
import { $projectId, $projectName, $projectUrl } from 'app/store/nanostores/projectId';
import { $queueId, DEFAULT_QUEUE_ID } from 'app/store/nanostores/queueId';
import { $store } from 'app/store/nanostores/store';
import { $toastMap } from 'app/store/nanostores/toastMap';
import { $whatsNew } from 'app/store/nanostores/whatsNew';
import { createStore } from 'app/store/store';
import type { PartialAppConfig } from 'app/types/invokeai';
@@ -32,6 +33,7 @@ import {
DEFAULT_WORKFLOW_LIBRARY_TAG_CATEGORIES,
} from 'features/nodes/store/workflowLibrarySlice';
import type { WorkflowCategory } from 'features/nodes/types/workflow';
import type { ToastConfig } from 'features/toast/toast';
import type { PropsWithChildren, ReactNode } from 'react';
import React, { lazy, memo, useEffect, useLayoutEffect, useMemo } from 'react';
import { Provider } from 'react-redux';
@@ -59,6 +61,7 @@ interface Props extends PropsWithChildren {
socketOptions?: Partial<ManagerOptions & SocketOptions>;
isDebugging?: boolean;
logo?: ReactNode;
toastMap?: Record<string, ToastConfig>;
whatsNew?: ReactNode[];
workflowCategories?: WorkflowCategory[];
workflowTagCategories?: WorkflowTagCategory[];
@@ -87,6 +90,7 @@ const InvokeAIUI = ({
socketOptions,
isDebugging = false,
logo,
toastMap,
workflowCategories,
workflowTagCategories,
workflowSortOptions,
@@ -227,6 +231,16 @@ const InvokeAIUI = ({
};
}, [logo]);
useEffect(() => {
if (toastMap) {
$toastMap.set(toastMap);
}
return () => {
$toastMap.set(undefined);
};
}, [toastMap]);
useEffect(() => {
if (whatsNew) {
$whatsNew.set(whatsNew);

View File

@@ -0,0 +1,4 @@
import type { ToastConfig } from 'features/toast/toast';
import { atom } from 'nanostores';
export const $toastMap = atom<Record<string, ToastConfig> | undefined>(undefined);

View File

@@ -9,7 +9,7 @@ export const toastApi = createStandaloneToast({
}).toast;
// Slightly modified version of UseToastOptions
type ToastConfig = Omit<UseToastOptions, 'id'> & {
export type ToastConfig = Omit<UseToastOptions, 'id'> & {
// Only string - Chakra allows numbers
id?: string;
};

View File

@@ -1,9 +1,35 @@
import type { Middleware } from '@reduxjs/toolkit';
import { isRejectedWithValue } from '@reduxjs/toolkit';
import { $toastMap } from 'app/store/nanostores/toastMap';
import { toast } from 'features/toast/toast';
import { t } from 'i18next';
import { z } from 'zod';
const trialUsageErrorSubstring = 'usage allotment for the free trial';
const trialUsageErrorCode = 'USAGE_LIMIT_TRIAL';
const orgUsageErrorSubstring = 'organization has reached its predefined usage allotment';
const orgUsageErrorCode = 'USAGE_LIMIT_ORG';
const indieUsageErrorSubstring = 'usage allotment';
const indieUsageErrorCode = 'USAGE_LIMIT_INDIE';
//TODO make this dynamic with returned error codes instead of substring check
const getErrorCode = (errorString?: string) => {
if (!errorString) {
return undefined;
}
if (errorString.includes(trialUsageErrorSubstring)) {
return trialUsageErrorCode;
}
if (errorString.includes(orgUsageErrorSubstring)) {
return orgUsageErrorCode;
}
if (errorString.includes(indieUsageErrorSubstring)) {
return indieUsageErrorCode;
}
};
const zRejectedForbiddenAction = z.object({
payload: z.object({
status: z.literal(403),
@@ -31,14 +57,21 @@ export const authToastMiddleware: Middleware = () => (next) => (action) => {
// do not show toast if problem is image access
return next(action);
}
const toastMap = $toastMap.get();
const customMessage = parsed.payload.data.detail !== 'Forbidden' ? parsed.payload.data.detail : undefined;
toast({
id: `auth-error-toast-${endpointName}`,
title: t('toast.somethingWentWrong'),
status: 'error',
description: customMessage,
});
const errorCode = getErrorCode(customMessage);
const customToastConfig = errorCode ? toastMap?.[errorCode] : undefined;
if (customToastConfig) {
toast(customToastConfig);
} else {
toast({
id: `auth-error-toast-${endpointName}`,
title: t('toast.somethingWentWrong'),
status: 'error',
description: customMessage,
});
}
} catch (error) {
// no-op
}