diff --git a/package-lock.json b/package-lock.json index bb5eb58..3bf5271 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,8 @@ "@next/bundle-analyzer": "^13.1.6", "@next/font": "^13.1.6", "@reduxjs/toolkit": "^1.9.2", - "@tinystacks/ops-core": "^0.3.2", - "@tinystacks/ops-model": "^0.4.0", + "@tinystacks/ops-core": "^0.4.0", + "@tinystacks/ops-model": "^0.5.0", "@types/node": "18.11.18", "@types/react": "18.0.27", "@types/react-dom": "18.0.10", @@ -36,6 +36,7 @@ "lodash.isempty": "^4.4.0", "lodash.isequal": "^4.4.0", "lodash.kebabcase": "^4.1.1", + "lodash.upperfirst": "^4.3.1", "next": "13.2.4", "next-compose-plugins": "^2.2.1", "next-optimized-images": "^2.6.2", @@ -60,6 +61,7 @@ "@types/lodash.isempty": "^4.4.7", "@types/lodash.isequal": "^4.4.7", "@types/lodash.kebabcase": "^4.1.7", + "@types/lodash.upperfirst": "^4.3.7", "@types/react-redux": "^7.1.25", "@types/react-router-dom": "^5.3.3", "@typescript-eslint/eslint-plugin": "^5.50.0", @@ -4219,19 +4221,21 @@ } }, "node_modules/@tinystacks/ops-core": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@tinystacks/ops-core/-/ops-core-0.3.2.tgz", - "integrity": "sha512-YTN2KHgtQgf3srcn8yH87kmvRqr/W5DSEWsi6fKFy+IoSd7/Mpv7QEijwadgNvlqEPbE9gixqERxsDatKcSCgA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@tinystacks/ops-core/-/ops-core-0.4.0.tgz", + "integrity": "sha512-j1QdSHzJ2YhQiRacArfKNkrvzcksWsZAXTDiliZMTBsNbtbRQVpfbkOowHb1B/iUU6hOENEh83u8LTW0TOH59A==", "dependencies": { - "@tinystacks/ops-model": "^0.4.0", + "@tinystacks/ops-model": "^0.5.0", "@types/react": "^18.0.28", + "http-status-codes": "^2.2.0", "lodash.get": "^4.4.2", "lodash.isnil": "^4.0.0" } }, "node_modules/@tinystacks/ops-core/node_modules/@types/react": { - "version": "18.2.0", - "license": "MIT", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.13.tgz", + "integrity": "sha512-vJ+zElvi/Zn9cVXB5slX2xL8PZodPCwPRDpittQdw43JR2AJ5k3vKdgJJyneV/cYgIbLQUwXa9JVDvUZXGba+Q==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -4239,9 +4243,9 @@ } }, "node_modules/@tinystacks/ops-model": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@tinystacks/ops-model/-/ops-model-0.4.0.tgz", - "integrity": "sha512-zh2BM/SLMdpH0ataWWmDzxgW/YGnRrBZ3f/4W/C6joFsNk4Yk7MMlY70Xfj8Z3kMLyauZ6ATK2GZbXbPP42z7w==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@tinystacks/ops-model/-/ops-model-0.5.0.tgz", + "integrity": "sha512-gsAbsHo6qX5StiH9U2DZJJ77bHhOy/u1fZ+CJPunBbkAPBbBFWwtizJ2PZuTrY7RfjiWUzudglpKi68AfqptUA==", "dependencies": { "openapi-typescript-codegen": "^0.23.0" } @@ -4477,6 +4481,15 @@ "@types/lodash": "*" } }, + "node_modules/@types/lodash.upperfirst": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@types/lodash.upperfirst/-/lodash.upperfirst-4.3.7.tgz", + "integrity": "sha512-CrBjoB4lO6h7tXNMBUl1eh/w0KdMosiEOXOoD5DMECsA/kDWo/WQfOt1KyGKVvgwK3I6cKAY6z8LymKiMazLFg==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/memcached": { "version": "2.2.7", "dev": true, @@ -9494,6 +9507,11 @@ "node": ">= 6" } }, + "node_modules/http-status-codes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.2.0.tgz", + "integrity": "sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng==" + }, "node_modules/https-browserify": { "version": "1.0.0", "license": "MIT", @@ -12260,7 +12278,8 @@ }, "node_modules/lodash.get": { "version": "4.4.2", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, "node_modules/lodash.isempty": { "version": "4.4.0", @@ -12272,7 +12291,8 @@ }, "node_modules/lodash.isnil": { "version": "4.0.0", - "license": "MIT" + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" }, "node_modules/lodash.kebabcase": { "version": "4.1.1", @@ -12287,6 +12307,11 @@ "version": "4.6.2", "license": "MIT" }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==" + }, "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", @@ -20278,18 +20303,21 @@ "requires": {} }, "@tinystacks/ops-core": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@tinystacks/ops-core/-/ops-core-0.3.2.tgz", - "integrity": "sha512-YTN2KHgtQgf3srcn8yH87kmvRqr/W5DSEWsi6fKFy+IoSd7/Mpv7QEijwadgNvlqEPbE9gixqERxsDatKcSCgA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@tinystacks/ops-core/-/ops-core-0.4.0.tgz", + "integrity": "sha512-j1QdSHzJ2YhQiRacArfKNkrvzcksWsZAXTDiliZMTBsNbtbRQVpfbkOowHb1B/iUU6hOENEh83u8LTW0TOH59A==", "requires": { - "@tinystacks/ops-model": "^0.4.0", + "@tinystacks/ops-model": "^0.5.0", "@types/react": "^18.0.28", + "http-status-codes": "^2.2.0", "lodash.get": "^4.4.2", "lodash.isnil": "^4.0.0" }, "dependencies": { "@types/react": { - "version": "18.2.0", + "version": "18.2.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.13.tgz", + "integrity": "sha512-vJ+zElvi/Zn9cVXB5slX2xL8PZodPCwPRDpittQdw43JR2AJ5k3vKdgJJyneV/cYgIbLQUwXa9JVDvUZXGba+Q==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -20299,9 +20327,9 @@ } }, "@tinystacks/ops-model": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@tinystacks/ops-model/-/ops-model-0.4.0.tgz", - "integrity": "sha512-zh2BM/SLMdpH0ataWWmDzxgW/YGnRrBZ3f/4W/C6joFsNk4Yk7MMlY70Xfj8Z3kMLyauZ6ATK2GZbXbPP42z7w==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@tinystacks/ops-model/-/ops-model-0.5.0.tgz", + "integrity": "sha512-gsAbsHo6qX5StiH9U2DZJJ77bHhOy/u1fZ+CJPunBbkAPBbBFWwtizJ2PZuTrY7RfjiWUzudglpKi68AfqptUA==", "requires": { "openapi-typescript-codegen": "^0.23.0" } @@ -20497,6 +20525,15 @@ "@types/lodash": "*" } }, + "@types/lodash.upperfirst": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@types/lodash.upperfirst/-/lodash.upperfirst-4.3.7.tgz", + "integrity": "sha512-CrBjoB4lO6h7tXNMBUl1eh/w0KdMosiEOXOoD5DMECsA/kDWo/WQfOt1KyGKVvgwK3I6cKAY6z8LymKiMazLFg==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, "@types/memcached": { "version": "2.2.7", "dev": true, @@ -23983,6 +24020,11 @@ "debug": "4" } }, + "http-status-codes": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.2.0.tgz", + "integrity": "sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng==" + }, "https-browserify": { "version": "1.0.0", "peer": true @@ -25677,7 +25719,9 @@ "version": "4.3.0" }, "lodash.get": { - "version": "4.4.2" + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" }, "lodash.isempty": { "version": "4.4.0" @@ -25686,7 +25730,9 @@ "version": "4.5.0" }, "lodash.isnil": { - "version": "4.0.0" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lodash.isnil/-/lodash.isnil-4.0.0.tgz", + "integrity": "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==" }, "lodash.kebabcase": { "version": "4.1.1", @@ -25699,6 +25745,11 @@ "lodash.mergewith": { "version": "4.6.2" }, + "lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==" + }, "loose-envify": { "version": "1.4.0", "requires": { diff --git a/package.json b/package.json index b1f8841..7a2e9d4 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,8 @@ "@next/bundle-analyzer": "^13.1.6", "@next/font": "^13.1.6", "@reduxjs/toolkit": "^1.9.2", - "@tinystacks/ops-core": "^0.3.2", - "@tinystacks/ops-model": "^0.4.0", + "@tinystacks/ops-core": "^0.4.0", + "@tinystacks/ops-model": "^0.5.0", "@types/node": "18.11.18", "@types/react": "18.0.27", "@types/react-dom": "18.0.10", @@ -47,6 +47,7 @@ "lodash.isempty": "^4.4.0", "lodash.isequal": "^4.4.0", "lodash.kebabcase": "^4.1.1", + "lodash.upperfirst": "^4.3.1", "next": "13.2.4", "next-compose-plugins": "^2.2.1", "next-optimized-images": "^2.6.2", @@ -71,6 +72,7 @@ "@types/lodash.isempty": "^4.4.7", "@types/lodash.isequal": "^4.4.7", "@types/lodash.kebabcase": "^4.1.7", + "@types/lodash.upperfirst": "^4.3.7", "@types/react-redux": "^7.1.25", "@types/react-router-dom": "^5.3.3", "@typescript-eslint/eslint-plugin": "^5.50.0", diff --git a/src/components/common/dismissable-error-banner.tsx b/src/components/common/dismissable-error-banner.tsx index 54e76fc..984da97 100644 --- a/src/components/common/dismissable-error-banner.tsx +++ b/src/components/common/dismissable-error-banner.tsx @@ -1,19 +1,20 @@ import { Alert, + Box, Container, Center, Flex, AlertIcon, AlertTitle, AlertDescription, - CloseButton + CloseButton, + Text } from '@chakra-ui/react'; +import upperFirst from 'lodash.upperfirst'; +import { ShowableError } from 'ops-frontend/types'; type DismissableErrorBannerProps = { - error: { - title: string; - message: string; - }, + error: ShowableError, dismissError: () => void; }; @@ -21,16 +22,53 @@ export default function DismissableErrorBanner (props: DismissableErrorBannerPro const { error: { title, - message + message, + cause, + context, + fields = {} }, dismissError } = props; + let extraDetail; + if (cause || context) { + const punctuationCheck = new RegExp('.*[,.?!;:=-]'); + let causePunctuation; + if (cause) { + causePunctuation = punctuationCheck.test(cause) ? '' : + ( + context ? ':' : '.' + ); + } + extraDetail = ( + + + {`${upperFirst(cause)}${causePunctuation}`} + + + { + context ? context : + Object.entries(fields).map(([property, propError]) => { + return `${property} - ${propError}` + }).join('\n') + } + + + ) + } + return ( - +
- +
+ {extraDetail}
); } \ No newline at end of file diff --git a/src/components/dashboard/dashboard-list.tsx b/src/components/dashboard/dashboard-list.tsx index 96eb633..4dd6313 100644 --- a/src/components/dashboard/dashboard-list.tsx +++ b/src/components/dashboard/dashboard-list.tsx @@ -16,7 +16,7 @@ import { dismissError, selectConsoleName, selectDashboards, - selectErropr, + selectError, updateConsole } from 'ops-frontend/store/consoleSlice'; import { useAppDispatch, useAppSelector } from 'ops-frontend/store/hooks'; @@ -25,15 +25,16 @@ import { FullpageLayout } from 'ops-frontend/components/layout/fullpage-layout'; import { DashboardCard } from 'ops-frontend/components/dashboard/dashboard-card'; import CreateDashboardModal from 'ops-frontend/components/dashboard/create-dashboard-modal'; import DismissableErrorBanner from 'ops-frontend/components/common/dismissable-error-banner'; +import { ShowableError } from 'ops-frontend/types'; export function DashboardList () { const { t: hm } = useTranslation('home'); const { t: d } = useTranslation('dashboard'); const dashboards = useAppSelector(selectDashboards); const consoleName = useAppSelector(selectConsoleName); - const error = useAppSelector(selectErropr); + const error = useAppSelector(selectError); const dispatch = useAppDispatch(); - const [consolesError, setConsolesError] = useState(undefined); + const [consolesError, setConsolesError] = useState(undefined); const [retryCount, setRetryCount] = useState(0); async function fetchData() { try { @@ -45,7 +46,17 @@ export function DashboardList () { setRetryCount(0); } } catch (e: any) { - setConsolesError(e.message); + const { + message, + cause, + context + } = e?.body?.body || {}; + setConsolesError({ + title: hm('consolesError'), + message, + cause, + context + }); setRetryCount(retryCount + 1); } } @@ -97,7 +108,7 @@ export function DashboardList () { errorBanner = ( setConsolesError(undefined)} /> ); diff --git a/src/components/dashboard/dashboard-wrapper.tsx b/src/components/dashboard/dashboard-wrapper.tsx index 3927f58..591da13 100644 --- a/src/components/dashboard/dashboard-wrapper.tsx +++ b/src/components/dashboard/dashboard-wrapper.tsx @@ -1,4 +1,4 @@ -import { selectConsoleName, selectDashboards, updateConsole, updateDashboard } from 'ops-frontend/store/consoleSlice'; +import { dismissError, handleError, selectConsoleName, selectDashboards, selectError, updateConsole, updateDashboard } from 'ops-frontend/store/consoleSlice'; import React, { ReactNode, useEffect, useState } from 'react'; import { useAppDispatch, useAppSelector } from 'ops-frontend/store/hooks'; import apis from 'ops-frontend/utils/apis'; @@ -9,8 +9,9 @@ import { FullpageLayout } from 'ops-frontend/components/layout/fullpage-layout'; import { SettingsIcon } from '@chakra-ui/icons'; import { useTranslation } from 'react-i18next'; import DashboardSettings from 'ops-frontend/components/dashboard/dashboard-settings'; -import { Dashboard } from '@tinystacks/ops-model'; +import { ApiError, Dashboard } from '@tinystacks/ops-model'; import { useNavigate } from 'react-router-dom'; +import DismissableErrorBanner from 'ops-frontend/components/common/dismissable-error-banner'; export function DashboardWrapper(props: { dashboardContents: ReactNode, dashboardId: string }) { const { dashboardContents, dashboardId } = props; @@ -19,6 +20,7 @@ export function DashboardWrapper(props: { dashboardContents: ReactNode, dashboar const consoleName = useAppSelector(selectConsoleName); const [retryCount, setRetryCount] = useState(0); const { t } = useTranslation('dashboard'); + const error = useAppSelector(selectError); const dispatch = useAppDispatch(); @@ -38,6 +40,13 @@ export function DashboardWrapper(props: { dashboardContents: ReactNode, dashboar } } catch (e) { setRetryCount(retryCount + 1); + if (retryCount >= 2) { + const error = (e as any).body as ApiError; + dispatch(handleError({ + title: 'Failed to fetch console!', + error: error?.body || error + })); + } } } @@ -90,11 +99,23 @@ export function DashboardWrapper(props: { dashboardContents: ReactNode, dashboar ) } + let errorBanner = (<>); + if (error) { + errorBanner = ( + dispatch(dismissError())} + /> + ); + } + return ( <> {renderHeader()} + {errorBanner} {content} diff --git a/src/components/dashboard/dashboard.tsx b/src/components/dashboard/dashboard.tsx index f9b2514..557e5ca 100644 --- a/src/components/dashboard/dashboard.tsx +++ b/src/components/dashboard/dashboard.tsx @@ -8,7 +8,7 @@ import { useRouter } from 'next/router' import { DashboardWrapper } from 'ops-frontend/components/dashboard/dashboard-wrapper'; import { selectConsoleName, selectConsoleWidgets, selectDependencies, selectHydratedWidgets, selectDashboard, - selectDashboardIdFromRoute, selectDashboardWidgets, updateHydratedWidget + selectDashboardIdFromRoute, selectDashboardWidgets, updateHydratedWidget, handleError } from 'ops-frontend/store/consoleSlice'; import { useAppSelector } from 'ops-frontend/store/hooks'; import { useTranslation } from 'react-i18next'; @@ -16,7 +16,8 @@ import { useEffect, useRef } from 'react'; import { useAppDispatch } from 'ops-frontend/store/hooks'; import { AppDispatch } from 'ops-frontend/store/store'; import { FullpageLayout } from 'ops-frontend/components/layout/fullpage-layout'; -import { Parameter, Widget, TinyStacksError } from '@tinystacks/ops-model'; +import { Parameter, Widget, TinyStacksError as TinyStacksErrorType } from '@tinystacks/ops-model'; +import { TinyStacksError } from '@tinystacks/ops-core'; import { FlatMap, Json, WidgetMap } from 'ops-frontend/types'; import ErrorWidget from 'ops-frontend/widgets/error-widget'; import LoadingWidget from 'ops-frontend/widgets/loading-widget'; @@ -73,18 +74,25 @@ function Dashboard() { useEffect(() => { async function importAndRenderWidgets() { - const deepRenderedWidgets = { ...renderedWidgets }; - for (let widget of dashboardWidgets) { - deepRenderedWidgets[widget.id || ''] = await renderWidgetAndChildren( - widget, - hydratedWidgets, - dependencies, - dashboardId, - parameters - ); + try { + const deepRenderedWidgets = { ...renderedWidgets }; + for (let widget of dashboardWidgets) { + deepRenderedWidgets[widget.id || ''] = await renderWidgetAndChildren( + widget, + hydratedWidgets, + dependencies, + dashboardId, + parameters + ); + } + + setRenderedWidgets(deepRenderedWidgets); + } catch (error: any) { + dispatch(handleError({ + title: 'Could not render widgets!', + error: error?.body || error + })); } - - setRenderedWidgets(deepRenderedWidgets); } // TODO: deep compare widget trees that are rendered on this dashboard instead @@ -252,7 +260,7 @@ async function renderWidget( { ...widget, originalType: widget.type, - error: (widget as TinyStacksError).message || '' + error: (widget as TinyStacksErrorType).message || '' } ) } else if (widget.type === 'LoadingWidget') { @@ -266,6 +274,13 @@ async function renderWidget( const moduleName = dependencies[widget.type]; const moduleNamespace = camelCase(moduleName); const plugin = (plugins as any)[moduleNamespace] as any; + if (!plugin) { + throw TinyStacksError.fromJson({ + message: 'Missing dependency!', + status: 424, + cause: `Cannot find module ${moduleName} for widget type ${widget.type} used in ${widget.id}.` + }).toJson(); + } hydratedWidget = plugin[widget.type].fromJson(widget); } diff --git a/src/i18n/en/home.ts b/src/i18n/en/home.ts index fa2b24d..f958f84 100644 --- a/src/i18n/en/home.ts +++ b/src/i18n/en/home.ts @@ -4,7 +4,7 @@ const home: LocaleMessageType = { title: 'Ops Console', dashboards: 'All Dashboards', addDashboard: 'Add Dashboard', - consolesError: 'Error parsing console' + consolesError: 'Error fetching consoles!' }; export default home; diff --git a/src/store/consoleSlice.ts b/src/store/consoleSlice.ts index 17ddb97..5cb7cae 100644 --- a/src/store/consoleSlice.ts +++ b/src/store/consoleSlice.ts @@ -1,5 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { ApiError, Console, Dashboard, TinyStacksError, Widget } from '@tinystacks/ops-model'; +import { TinyStacksError } from '@tinystacks/ops-core'; +import { ApiError, Console, Dashboard, Widget } from '@tinystacks/ops-model'; import { AppDispatch, RootState } from 'ops-frontend/store/store'; import { ShowableError, WidgetMap } from 'ops-frontend/types'; import apis from 'ops-frontend/utils/apis'; @@ -15,7 +16,7 @@ export const createNewDashboard = (consoleName: string, dashboard: Dashboard) => await dispatch(removeTempDashboard(dashboard)); return dispatch(handleError({ title: 'Failed to create dashboard!', - message: error?.body?.message || error?.message + error: error?.body || error })); } } @@ -29,10 +30,10 @@ export const fetchConsoles = (consoleName?: string) => async (dispatch: AppDispa consoles.at(0); return dispatch(updateConsole(console || {} as Console)); } catch (e) { - const error = e as TinyStacksError; + const error = (e as any).body as ApiError; return handleError({ - title: 'Failed to create dashboard!', - message: error.message || '' + title: 'Failed to fetch consoles!', + error: error?.body || error }); } } @@ -46,7 +47,7 @@ export const updateDashboard = (consoleName: string, dashboard: Dashboard, dashb const error = (e as any).body as ApiError; return dispatch(handleError({ title: 'Failed to update dashboard!', - message: error?.body?.message || error?.message + error: error?.body || error })); } } @@ -59,7 +60,7 @@ export const fetchDashboards = (consoleName: string) => async (dispatch: AppDisp const error = (e as any).body as ApiError; return dispatch(handleError({ title: 'Failed to fetch dashboards!', - message: error?.body?.message || error?.message + error: error?.body })); } } @@ -144,8 +145,18 @@ export const consoleSlice = createSlice({ overrides: widgetOverrides } }, - handleError: function (state: ConsoleSliceState, action: PayloadAction) { - state.error = action.payload; + handleError: function (state: ConsoleSliceState, action: PayloadAction<{ title: string; error: any }>) { + if (TinyStacksError.isTinyStacksError(action.payload.error)) { + state.error = { + title: action.payload.title, + ...action.payload.error + }; + } else { + state.error = { + title: action.payload.title, + message: action.payload.error.message + }; + } }, dismissError: function (state: ConsoleSliceState) { state.error = undefined; @@ -242,7 +253,7 @@ export function selectWidget(widgetId: string) { } } -export function selectErropr(state: RootState): ShowableError | undefined { +export function selectError(state: RootState): ShowableError | undefined { return state.console.error; } diff --git a/src/types.ts b/src/types.ts index 8f8ee10..4f9d429 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,4 @@ -import { Widget } from '@tinystacks/ops-model'; +import { TinyStacksError, Widget } from '@tinystacks/ops-model'; export type WidgetMap = { [id: string]: Widget }; export type FlatMap = { [id: string]: string }; @@ -10,7 +10,6 @@ export type GetWidgetArguments = { dashboardId?: string; parameters?: Json; }; -export type ShowableError = { +export type ShowableError = Omit & { title: string; - message: string; } \ No newline at end of file diff --git a/src/utils/handle-response.ts b/src/utils/handle-response.ts index 7d8d547..4854aa9 100644 --- a/src/utils/handle-response.ts +++ b/src/utils/handle-response.ts @@ -1,11 +1,12 @@ +import { TinyStacksError } from '@tinystacks/ops-core'; import type { NextApiResponse } from 'next' -import { isTinyStacksError } from 'ops-frontend/utils/is-tinystacks-error'; + export function handleResponse ( clientResponse: T, res: NextApiResponse ) { - if (isTinyStacksError(clientResponse)) { + if (TinyStacksError.isTinyStacksError(clientResponse)) { throw clientResponse; } res.status(200).send(clientResponse) diff --git a/src/utils/is-tinystacks-error.ts b/src/utils/is-tinystacks-error.ts deleted file mode 100644 index a6cadbc..0000000 --- a/src/utils/is-tinystacks-error.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { TinyStacksError } from '@tinystacks/ops-model'; - -const TinyStacksErrorName = 'TinyStacksError'; - -export function isTinyStacksError (error: unknown): boolean { - const e = error as any; - const hasTinyStacksErrorName: boolean = e?.name && e?.name === TinyStacksErrorName; - const hasTinyStacksErrorType: boolean = e?.type && Object.values(TinyStacksError.type).includes(e?.type); - const hasMessage: boolean = e?.message && typeof e?.message === 'string'; - const hasStatus: boolean = e?.status && typeof e?.status === 'number'; - const isTsError: boolean = ( - hasTinyStacksErrorName && - hasTinyStacksErrorType && - hasMessage && - hasStatus - ); - return isTsError; -} \ No newline at end of file