Dashboards: Change the way dashboard not found error is handled (#98950)
* Get rid of _dashboardLoadFailed * Get rid of dashboardNotFound meta * Update public dashboards tests * Fix DashboardPage tests * DashboardPageProxy tests * DashboardScenePageStateManager test fix * Beterer * Fix merge * Nits * Fix test * remove debugger * Update get folder to throw * translate error title * Update public/app/features/apiserver/types.ts Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com> * Update public/app/features/dashboard/services/DashboardLoaderSrv.ts Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com> * Update public/app/features/dashboard/services/DashboardLoaderSrv.ts Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com> * Update public/app/features/dashboard/services/DashboardLoaderSrv.ts Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com> * Betterer * Update test cases * More test updates * More translations --------- Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com> Co-authored-by: Haris Rozajac <58232930+harisrozajac@users.noreply.github.com>
This commit is contained in:
co-authored by
Ivan Ortega Alba
Haris Rozajac
parent
571f20676d
commit
ae62b3817b
+4
-5
@@ -3236,9 +3236,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/embedding/EmbeddedDashboard.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/embedding/EmbeddedDashboardTestPage.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
@@ -3281,8 +3278,7 @@ exports[`better eslint`] = {
|
||||
"public/app/features/dashboard-scene/pages/DashboardScenePage.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/panel-edit/LibraryVizPanelInfo.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
@@ -4213,6 +4209,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/dashboard/containers/PublicDashboardPage.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard/containers/SoloPanelPage.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FetchError } from '@grafana/runtime';
|
||||
import { getMessageFromError } from 'app/core/utils/errors';
|
||||
import { LoadError } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
|
||||
|
||||
describe('errors functions', () => {
|
||||
let message: string | null;
|
||||
@@ -53,4 +54,18 @@ describe('errors functions', () => {
|
||||
expect(message).toBe('{"customError":"error string"}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when getMessageFromError gets an LoadError object', () => {
|
||||
beforeEach(() => {
|
||||
const error: LoadError = {
|
||||
message: 'error string',
|
||||
status: 500,
|
||||
};
|
||||
message = getMessageFromError(error);
|
||||
});
|
||||
|
||||
it('should return the stringified error', () => {
|
||||
expect(message).toBe('error string');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,8 +14,49 @@ export function getMessageFromError(err: unknown): string {
|
||||
} else if (err.statusText) {
|
||||
return err.statusText;
|
||||
}
|
||||
} else if (err.hasOwnProperty('message')) {
|
||||
// @ts-expect-error
|
||||
return err.message;
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(err);
|
||||
}
|
||||
|
||||
export function getStatusFromError(err: unknown): number | undefined {
|
||||
if (typeof err === 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
if (err instanceof Error) {
|
||||
return undefined;
|
||||
} else if (isFetchError(err)) {
|
||||
return err.status;
|
||||
} else if (err.hasOwnProperty('status')) {
|
||||
// @ts-expect-error
|
||||
return err.status;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getMessageIdFromError(err: unknown): string | undefined {
|
||||
if (typeof err === 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (err) {
|
||||
if (err instanceof Error) {
|
||||
return undefined;
|
||||
} else if (isFetchError(err)) {
|
||||
return err.data?.messageId;
|
||||
} else if (err.hasOwnProperty('messageId')) {
|
||||
// @ts-expect-error
|
||||
return err.messageId;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@ type GrafanaClientAnnotations = {
|
||||
[AnnoKeyFolderId]?: number;
|
||||
[AnnoKeyFolderId]?: number;
|
||||
[AnnoKeySavedFromUI]?: string;
|
||||
[AnnoKeyDashboardNotFound]?: boolean;
|
||||
[AnnoKeyDashboardIsSnapshot]?: boolean;
|
||||
[AnnoKeyDashboardSnapshotOriginalUrl]?: string;
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { GrafanaTheme2, urlUtil } from '@grafana/data';
|
||||
import { EmbeddedDashboardProps } from '@grafana/runtime';
|
||||
import { SceneObjectStateChangedEvent, sceneUtils } from '@grafana/scenes';
|
||||
import { Spinner, Alert, useStyles2 } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { getMessageFromError } from 'app/core/utils/errors';
|
||||
import { DashboardRoutes } from 'app/types';
|
||||
|
||||
import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager';
|
||||
@@ -23,8 +25,8 @@ export function EmbeddedDashboard(props: EmbeddedDashboardProps) {
|
||||
|
||||
if (loadError) {
|
||||
return (
|
||||
<Alert severity="error" title="Failed to load dashboard">
|
||||
{loadError}
|
||||
<Alert severity="error" title={t('dashboard.errors.failed-to-load', 'Failed to load dashboard')}>
|
||||
{getMessageFromError(loadError)}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||
|
||||
import { PanelProps } from '@grafana/data';
|
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import {
|
||||
LocationServiceProvider,
|
||||
config,
|
||||
@@ -23,6 +24,7 @@ import { DashboardLoaderSrv, setDashboardLoaderSrv } from 'app/features/dashboar
|
||||
import { DASHBOARD_FROM_LS_KEY, DashboardRoutes } from 'app/types';
|
||||
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { setupLoadDashboardMockReject, setupLoadDashboardRuntimeErrorMock } from '../utils/test-utils';
|
||||
|
||||
import { DashboardScenePage, Props } from './DashboardScenePage';
|
||||
import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager';
|
||||
@@ -298,6 +300,64 @@ describe('DashboardScenePage', () => {
|
||||
await waitFor(() => expect(screen.queryByText('Last 6 hours')).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors rendering', () => {
|
||||
it('should render dashboard not found notice when dashboard... not found', async () => {
|
||||
setupLoadDashboardMockReject({
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
data: {
|
||||
message: 'Dashboard not found',
|
||||
},
|
||||
config: {
|
||||
method: 'GET',
|
||||
url: 'api/dashboards/uid/adfjq9edwm0hsdsa',
|
||||
retry: 0,
|
||||
headers: {
|
||||
'X-Grafana-Org-Id': 1,
|
||||
},
|
||||
hideFromInspector: true,
|
||||
},
|
||||
isHandled: true,
|
||||
});
|
||||
|
||||
setup();
|
||||
|
||||
expect(await screen.findByTestId(selectors.components.EntityNotFound.container)).toBeInTheDocument();
|
||||
});
|
||||
it('should render error alert for backend errors', async () => {
|
||||
setupLoadDashboardMockReject({
|
||||
status: 500,
|
||||
statusText: 'internal server error',
|
||||
data: {
|
||||
message: 'Internal server error',
|
||||
},
|
||||
config: {
|
||||
method: 'GET',
|
||||
url: 'api/dashboards/uid/adfjq9edwm0hsdsa',
|
||||
retry: 0,
|
||||
headers: {
|
||||
'X-Grafana-Org-Id': 1,
|
||||
},
|
||||
hideFromInspector: true,
|
||||
},
|
||||
isHandled: true,
|
||||
});
|
||||
|
||||
setup();
|
||||
|
||||
expect(await screen.findByTestId('dashboard-page-error')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('dashboard-page-error')).toHaveTextContent('Internal server error');
|
||||
});
|
||||
it('should render error alert for runtime errors', async () => {
|
||||
setupLoadDashboardRuntimeErrorMock();
|
||||
|
||||
setup();
|
||||
|
||||
expect(await screen.findByTestId('dashboard-page-error')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('dashboard-page-error')).toHaveTextContent('Runtime error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
interface VizOptions {
|
||||
|
||||
@@ -6,10 +6,11 @@ import { usePrevious } from 'react-use';
|
||||
import { PageLayoutType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { UrlSyncContextProvider } from '@grafana/scenes';
|
||||
import { Alert, Box } from '@grafana/ui';
|
||||
import { Box } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { DashboardPageError } from 'app/features/dashboard/containers/DashboardPageError';
|
||||
import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from 'app/features/dashboard/containers/types';
|
||||
import { DashboardRoutes } from 'app/types';
|
||||
|
||||
@@ -48,17 +49,19 @@ export function DashboardScenePage({ route, queryParams, location }: Props) {
|
||||
}, [stateManager, uid, route.routeName, queryParams.folderUid, routeReloadCounter, slug, type]);
|
||||
|
||||
if (!dashboard) {
|
||||
let errorElement;
|
||||
if (loadError) {
|
||||
errorElement = <DashboardPageError error={loadError} type={type} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page navId="dashboards/browse" layout={PageLayoutType.Canvas} data-testid={'dashboard-scene-page'}>
|
||||
<Box paddingY={4} display="flex" direction="column" alignItems="center">
|
||||
{isLoading && <PageLoader />}
|
||||
{loadError && (
|
||||
<Alert title="Dashboard failed to load" severity="error" data-testid="dashboard-not-found">
|
||||
{loadError}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
</Page>
|
||||
errorElement || (
|
||||
<Page navId="dashboards/browse" layout={PageLayoutType.Canvas} data-testid={'dashboard-scene-page'}>
|
||||
<Box paddingY={4} display="flex" direction="column" alignItems="center">
|
||||
{isLoading && <PageLoader />}
|
||||
</Box>
|
||||
</Page>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { getDashboardSnapshotSrv } from 'app/features/dashboard/services/Snapsho
|
||||
import { DASHBOARD_FROM_LS_KEY, DashboardRoutes } from 'app/types';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { setupLoadDashboardMock } from '../utils/test-utils';
|
||||
import { setupLoadDashboardMock, setupLoadDashboardMockReject } from '../utils/test-utils';
|
||||
|
||||
import {
|
||||
DashboardScenePageStateManager,
|
||||
@@ -45,14 +45,34 @@ describe('DashboardScenePageStateManager v1', () => {
|
||||
});
|
||||
|
||||
it("should error when the dashboard doesn't exist", async () => {
|
||||
setupLoadDashboardMock({ dashboard: undefined, meta: {} });
|
||||
setupLoadDashboardMockReject({
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
data: {
|
||||
message: 'Dashboard not found',
|
||||
},
|
||||
config: {
|
||||
method: 'GET',
|
||||
url: 'api/dashboards/uid/adfjq9edwm0hsdsa',
|
||||
retry: 0,
|
||||
headers: {
|
||||
'X-Grafana-Org-Id': 1,
|
||||
},
|
||||
hideFromInspector: true,
|
||||
},
|
||||
isHandled: true,
|
||||
});
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
|
||||
expect(loader.state.dashboard).toBeUndefined();
|
||||
expect(loader.state.isLoading).toBe(false);
|
||||
expect(loader.state.loadError).toBe('Dashboard not found');
|
||||
expect(loader.state.loadError).toEqual({
|
||||
status: 404,
|
||||
messageId: undefined,
|
||||
message: 'Dashboard not found',
|
||||
});
|
||||
});
|
||||
|
||||
it('should clear current dashboard while loading next', async () => {
|
||||
@@ -128,7 +148,11 @@ describe('DashboardScenePageStateManager v1', () => {
|
||||
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
|
||||
|
||||
expect(loader.state.dashboard).toBeUndefined();
|
||||
expect(loader.state.loadError).toEqual('Failed to load home dashboard');
|
||||
expect(loader.state.loadError).toEqual({
|
||||
message: 'Failed to load home dashboard',
|
||||
messageId: undefined,
|
||||
status: 500,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -425,7 +449,11 @@ describe('DashboardScenePageStateManager v2', () => {
|
||||
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
|
||||
|
||||
expect(loader.state.dashboard).toBeUndefined();
|
||||
expect(loader.state.loadError).toEqual('Failed to load home dashboard');
|
||||
expect(loader.state.loadError).toEqual({
|
||||
message: 'Failed to load home dashboard',
|
||||
messageId: undefined,
|
||||
status: 500,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { locationUtil, UrlQueryMap } from '@grafana/data';
|
||||
import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
||||
import { getMessageFromError } from 'app/core/utils/errors';
|
||||
import { getMessageFromError, getMessageIdFromError, getStatusFromError } from 'app/core/utils/errors';
|
||||
import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
|
||||
import { AnnoKeyFolder } from 'app/features/apiserver/types';
|
||||
import { ResponseTransformers } from 'app/features/dashboard/api/ResponseTransformers';
|
||||
@@ -24,12 +24,18 @@ import { restoreDashboardStateFromLocalStorage } from '../utils/dashboardSession
|
||||
|
||||
import { updateNavModel } from './utils';
|
||||
|
||||
export interface LoadError {
|
||||
status?: number;
|
||||
messageId?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface DashboardScenePageState {
|
||||
dashboard?: DashboardScene;
|
||||
options?: LoadDashboardOptions;
|
||||
panelEditor?: PanelEditor;
|
||||
isLoading?: boolean;
|
||||
loadError?: string;
|
||||
loadError?: LoadError;
|
||||
}
|
||||
|
||||
export const DASHBOARD_CACHE_TTL = 500;
|
||||
@@ -48,6 +54,7 @@ interface DashboardCacheEntry<T> {
|
||||
export interface LoadDashboardOptions {
|
||||
uid: string;
|
||||
route: DashboardRoutes;
|
||||
type?: string;
|
||||
urlFolderUid?: string;
|
||||
params?: {
|
||||
version: number;
|
||||
@@ -99,7 +106,18 @@ abstract class DashboardScenePageStateManagerBase<T>
|
||||
|
||||
this.setState({ dashboard: dashboard, isLoading: false });
|
||||
} catch (err) {
|
||||
this.setState({ isLoading: false, loadError: String(err) });
|
||||
const status = getStatusFromError(err);
|
||||
const message = getMessageFromError(err);
|
||||
const messageId = getMessageIdFromError(err);
|
||||
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
loadError: {
|
||||
status,
|
||||
message,
|
||||
messageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,8 +146,17 @@ abstract class DashboardScenePageStateManagerBase<T>
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = getMessageFromError(err);
|
||||
this.setState({ isLoading: false, loadError: msg });
|
||||
const status = getStatusFromError(err);
|
||||
const message = getMessageFromError(err);
|
||||
const messageId = getMessageIdFromError(err);
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
loadError: {
|
||||
status,
|
||||
message,
|
||||
messageId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +378,13 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
|
||||
}
|
||||
|
||||
if (!rsp?.dashboard) {
|
||||
this.setState({ isLoading: false, loadError: 'Dashboard not found' });
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
loadError: {
|
||||
status: 404,
|
||||
message: 'Dashboard not found',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -361,8 +394,15 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
|
||||
|
||||
this.setState({ dashboard: scene, isLoading: false, options });
|
||||
} catch (err) {
|
||||
const msg = getMessageFromError(err);
|
||||
this.setState({ isLoading: false, loadError: msg });
|
||||
const status = getStatusFromError(err);
|
||||
const message = getMessageFromError(err);
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
loadError: {
|
||||
message,
|
||||
status,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,7 +469,6 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
|
||||
urlFolderUid,
|
||||
params,
|
||||
}: LoadDashboardOptions): Promise<DashboardWithAccessInfo<DashboardV2Spec> | null> {
|
||||
// throw new Error('Method not implemented.');
|
||||
const cacheKey = route === DashboardRoutes.Home ? HOME_DASHBOARD_CACHE_KEY : uid;
|
||||
if (!params) {
|
||||
const cachedDashboard = this.getDashboardFromCache(cacheKey);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Dashboard } from '@grafana/schema';
|
||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||
import { DashboardRoutes } from 'app/types/dashboard';
|
||||
|
||||
import { setupLoadDashboardMock } from '../utils/test-utils';
|
||||
import { setupLoadDashboardMock, setupLoadDashboardMockReject } from '../utils/test-utils';
|
||||
|
||||
import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager';
|
||||
import { PublicDashboardScenePage, Props as PublicDashboardSceneProps } from './PublicDashboardScenePage';
|
||||
@@ -192,10 +192,27 @@ describe('given unavailable public dashboard', () => {
|
||||
it('renders public dashboard paused screen when it is paused', async () => {
|
||||
const accessToken = 'paused-pubdash-access-token';
|
||||
config.publicDashboardAccessToken = accessToken;
|
||||
setupLoadDashboardMock({
|
||||
dashboard: simpleDashboard,
|
||||
meta: { publicDashboardEnabled: false, dashboardNotFound: false },
|
||||
|
||||
setupLoadDashboardMockReject({
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
data: {
|
||||
statusCode: 403,
|
||||
messageId: 'publicdashboards.notEnabled',
|
||||
message: 'Dashboard paused',
|
||||
},
|
||||
config: {
|
||||
method: 'GET',
|
||||
url: 'api/public/dashboards/ce159fe139fc4d238a7d9c3ae33fb82b',
|
||||
retry: 0,
|
||||
headers: {
|
||||
'X-Grafana-Org-Id': 1,
|
||||
'X-Grafana-Device-Id': 'da48fad0e58ba327fd7d1e6bd17e9c63',
|
||||
},
|
||||
hideFromInspector: true,
|
||||
},
|
||||
});
|
||||
|
||||
setup(accessToken);
|
||||
|
||||
await waitForElementToBeRemoved(screen.getByTestId(publicDashboardSceneSelector.loadingPage));
|
||||
@@ -208,10 +225,26 @@ describe('given unavailable public dashboard', () => {
|
||||
it('renders public dashboard not available screen when it is deleted', async () => {
|
||||
const accessToken = 'deleted-pubdash-access-token';
|
||||
config.publicDashboardAccessToken = accessToken;
|
||||
setupLoadDashboardMock({
|
||||
dashboard: simpleDashboard,
|
||||
meta: { dashboardNotFound: true },
|
||||
|
||||
setupLoadDashboardMockReject({
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
data: {
|
||||
statusCode: 404,
|
||||
messageId: 'publicdashboards.notFound',
|
||||
message: 'Dashboard not found',
|
||||
},
|
||||
config: {
|
||||
method: 'GET',
|
||||
url: 'api/public/dashboards/ce159fe139fc4d238a7d9c3ae33fb82b',
|
||||
retry: 0,
|
||||
hideFromInspector: true,
|
||||
headers: {
|
||||
'X-Grafana-Device-Id': 'da48fad0e58ba327fd7d1e6bd17e9c63',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setup(accessToken);
|
||||
|
||||
await waitForElementToBeRemoved(screen.getByTestId(publicDashboardSceneSelector.loadingPage));
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useParams } from 'react-router-dom-v5-compat';
|
||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
|
||||
import { SceneComponentProps, UrlSyncContextProvider } from '@grafana/scenes';
|
||||
import { Icon, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, Box, Icon, Stack, useStyles2 } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
@@ -15,11 +15,12 @@ import {
|
||||
PublicDashboardPageRouteParams,
|
||||
PublicDashboardPageRouteSearchParams,
|
||||
} from 'app/features/dashboard/containers/types';
|
||||
import { AppNotificationSeverity } from 'app/types';
|
||||
import { DashboardRoutes } from 'app/types/dashboard';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
|
||||
import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager';
|
||||
import { getDashboardScenePageStateManager, LoadError } from './DashboardScenePageStateManager';
|
||||
|
||||
const selectors = e2eSelectors.pages.PublicDashboardScene;
|
||||
|
||||
@@ -42,23 +43,18 @@ export function PublicDashboardScenePage({ route }: Props) {
|
||||
};
|
||||
}, [stateManager, accessToken, route.routeName]);
|
||||
|
||||
if (loadError) {
|
||||
return <PublicDashboardScenePageError error={loadError} />;
|
||||
}
|
||||
|
||||
if (!dashboard) {
|
||||
return (
|
||||
<Page layout={PageLayoutType.Custom} className={styles.loadingPage} data-testid={selectors.loadingPage}>
|
||||
{isLoading && <PageLoader />}
|
||||
{loadError && <h2>{loadError}</h2>}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
if (dashboard.state.meta.publicDashboardEnabled === false) {
|
||||
return <PublicDashboardNotAvailable paused />;
|
||||
}
|
||||
|
||||
if (dashboard.state.meta.dashboardNotFound) {
|
||||
return <PublicDashboardNotAvailable />;
|
||||
}
|
||||
|
||||
// if no time picker render without url sync
|
||||
if (dashboard.state.controls?.state.hideTimeControls) {
|
||||
return <PublicDashboardSceneRenderer model={dashboard} />;
|
||||
@@ -162,3 +158,35 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function PublicDashboardScenePageError({ error }: { error: LoadError }) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const statusCode = error.status;
|
||||
const messageId = error.messageId;
|
||||
const message = error.message;
|
||||
|
||||
const isPublicDashboardPaused = statusCode === 403 && messageId === 'publicdashboards.notEnabled';
|
||||
const isPublicDashboardNotFound = statusCode === 404 && messageId === 'publicdashboards.notFound';
|
||||
const isDashboardNotFound = statusCode === 404 && messageId === 'publicdashboards.dashboardNotFound';
|
||||
|
||||
const publicDashboardEnabled = isPublicDashboardNotFound ? undefined : !isPublicDashboardPaused;
|
||||
const dashboardNotFound = isPublicDashboardNotFound || isDashboardNotFound;
|
||||
|
||||
if (publicDashboardEnabled === false) {
|
||||
return <PublicDashboardNotAvailable paused />;
|
||||
}
|
||||
|
||||
if (dashboardNotFound) {
|
||||
return <PublicDashboardNotAvailable />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page layout={PageLayoutType.Custom} className={styles.loadingPage} data-testid={selectors.loadingPage}>
|
||||
<Box paddingY={4} display="flex" direction="column" alignItems="center">
|
||||
<Alert severity={AppNotificationSeverity.Error} title={message}>
|
||||
{message}
|
||||
</Alert>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -421,10 +421,6 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
const { meta, viewPanelScene, editPanel, title, uid } = this.state;
|
||||
const isNew = !Boolean(uid);
|
||||
|
||||
if (meta.dashboardNotFound) {
|
||||
return { text: 'Not found' };
|
||||
}
|
||||
|
||||
let pageNav: NavModelItem = {
|
||||
text: title,
|
||||
url: getDashboardUrl({
|
||||
|
||||
@@ -2,7 +2,6 @@ import { screen } from '@testing-library/react';
|
||||
import { render } from 'test/test-utils';
|
||||
|
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config, setPluginImportUtils } from '@grafana/runtime';
|
||||
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
@@ -34,46 +33,6 @@ jest.mock('@grafana/runtime', () => ({
|
||||
}));
|
||||
|
||||
describe('DashboardSceneRenderer', () => {
|
||||
it('should render Not Found notice when dashboard is not found', async () => {
|
||||
const scene = transformSaveModelToScene({
|
||||
meta: {
|
||||
isSnapshot: true,
|
||||
dashboardNotFound: true,
|
||||
canStar: false,
|
||||
canDelete: false,
|
||||
canSave: false,
|
||||
canEdit: false,
|
||||
canShare: false,
|
||||
},
|
||||
dashboard: {
|
||||
title: 'Not found',
|
||||
uid: 'uid',
|
||||
schemaVersion: 0,
|
||||
// Disabling build in annotations to avoid mocking Grafana data source
|
||||
annotations: {
|
||||
list: [
|
||||
{
|
||||
builtIn: 1,
|
||||
datasource: {
|
||||
type: 'grafana',
|
||||
uid: '-- Grafana --',
|
||||
},
|
||||
enable: false,
|
||||
hide: true,
|
||||
iconColor: 'rgba(0, 211, 255, 1)',
|
||||
name: 'Annotations & Alerts',
|
||||
type: 'dashboard',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<scene.Component model={scene} />);
|
||||
|
||||
expect(await screen.findByTestId(selectors.components.EntityNotFound.container)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render angular deprecation notice when dashboard contains angular components', async () => {
|
||||
const noticeText = /This dashboard depends on Angular/i;
|
||||
//enable feature flag angularDeprecationUI
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useLocation, useParams } from 'react-router-dom-v5-compat';
|
||||
import { PageLayoutType } from '@grafana/data';
|
||||
import { SceneComponentProps } from '@grafana/scenes';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
|
||||
import { useSelector } from 'app/types';
|
||||
@@ -16,18 +15,8 @@ import { PanelSearchLayout } from './PanelSearchLayout';
|
||||
import { DashboardAngularDeprecationBanner } from './angular/DashboardAngularDeprecationBanner';
|
||||
|
||||
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
||||
const {
|
||||
controls,
|
||||
overlay,
|
||||
editview,
|
||||
editPanel,
|
||||
isEmpty,
|
||||
meta,
|
||||
viewPanelScene,
|
||||
panelSearch,
|
||||
panelsPerRow,
|
||||
isEditing,
|
||||
} = model.useState();
|
||||
const { controls, overlay, editview, editPanel, isEmpty, viewPanelScene, panelSearch, panelsPerRow, isEditing } =
|
||||
model.useState();
|
||||
const { type } = useParams();
|
||||
const location = useLocation();
|
||||
const navIndex = useSelector((state) => state.navIndex);
|
||||
@@ -60,10 +49,6 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
||||
}
|
||||
|
||||
function renderBody() {
|
||||
if (meta.dashboardNotFound) {
|
||||
return <EntityNotFound entity="Dashboard" key="dashboard-not-found" />;
|
||||
}
|
||||
|
||||
if (panelSearch || panelsPerRow) {
|
||||
return <PanelSearchLayout panelSearch={panelSearch} panelsPerRow={panelsPerRow} dashboard={model} />;
|
||||
}
|
||||
|
||||
@@ -178,14 +178,6 @@ describe('NavToolbarActions', () => {
|
||||
|
||||
expect(screen.queryByTestId('button-snapshot')).toBeInTheDocument();
|
||||
});
|
||||
it('should not show link button when is not found dashboard', () => {
|
||||
setup({
|
||||
isSnapshot: true,
|
||||
dashboardNotFound: true,
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId('button-snapshot')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
const isViewingPanel = Boolean(viewPanelScene);
|
||||
const isEditedPanelDirty = usePanelEditDirty(editPanel);
|
||||
const isEditingLibraryPanel = editPanel && isLibraryPanel(editPanel.state.panelRef.resolve());
|
||||
const isNotFound = Boolean(meta.dashboardNotFound);
|
||||
const isNew = !Boolean(uid);
|
||||
|
||||
const hasCopiedPanel = store.exists(LS_PANEL_COPY_KEY);
|
||||
@@ -80,10 +79,6 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
const showScopesSelector = config.featureToggles.scopeFilters && !isEditing;
|
||||
const dashboardNewLayouts = config.featureToggles.dashboardNewLayouts;
|
||||
|
||||
if (isNotFound) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isEditingPanel) {
|
||||
// This adds the precence indicators in enterprise
|
||||
addDynamicActions(toolbarActions, dynamicDashNavActions.left, 'left-actions');
|
||||
@@ -150,7 +145,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
|
||||
toolbarActions.push({
|
||||
group: 'icon-actions',
|
||||
condition: meta.isSnapshot && !meta.dashboardNotFound && !isEditing,
|
||||
condition: meta.isSnapshot && !isEditing,
|
||||
render: () => (
|
||||
<GoToSnapshotOriginButton key="go-to-snapshot-origin" originalURL={dashboard.getSnapshotUrl() ?? ''} />
|
||||
),
|
||||
|
||||
@@ -55,7 +55,6 @@ import {
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import {
|
||||
AnnoKeyCreatedBy,
|
||||
AnnoKeyDashboardNotFound,
|
||||
AnnoKeyFolder,
|
||||
AnnoKeyUpdatedBy,
|
||||
AnnoKeyUpdatedTimestamp,
|
||||
@@ -148,7 +147,6 @@ export function transformSaveModelSchemaV2ToScene(dto: DashboardWithAccessInfo<D
|
||||
showSettings: Boolean(dto.access.canEdit),
|
||||
canMakeEditable: canSave && !isDashboardEditable,
|
||||
hasUnsavedFolderChange: false,
|
||||
dashboardNotFound: Boolean(dto.metadata.annotations?.[AnnoKeyDashboardNotFound]),
|
||||
version: parseInt(metadata.resourceVersion, 10),
|
||||
k8s: metadata,
|
||||
};
|
||||
|
||||
@@ -5,9 +5,10 @@ import { useParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { UrlSyncContextProvider } from '@grafana/scenes';
|
||||
import { Alert, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import { Alert, Box, Spinner, useStyles2 } from '@grafana/ui';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { DashboardPageRouteParams } from 'app/features/dashboard/containers/types';
|
||||
import { DashboardRoutes } from 'app/types';
|
||||
@@ -24,7 +25,7 @@ export interface Props extends GrafanaRouteComponentProps<DashboardPageRoutePara
|
||||
*/
|
||||
export function SoloPanelPage({ queryParams }: Props) {
|
||||
const stateManager = getDashboardScenePageStateManager();
|
||||
const { dashboard } = stateManager.useState();
|
||||
const { dashboard, loadError } = stateManager.useState();
|
||||
const { uid = '' } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -36,6 +37,16 @@ export function SoloPanelPage({ queryParams }: Props) {
|
||||
return <EntityNotFound entity="Panel" />;
|
||||
}
|
||||
|
||||
if (loadError) {
|
||||
return (
|
||||
<Box justifyContent={'center'} alignItems={'center'} display={'flex'} height={'100%'}>
|
||||
<Alert severity="error" title={t('dashboard.errors.failed-to-load', 'Failed to load dashboard')}>
|
||||
{loadError.message}
|
||||
</Alert>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!dashboard) {
|
||||
return <PageLoader />;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { VariableRefresh } from '@grafana/data';
|
||||
import { FetchError } from '@grafana/runtime';
|
||||
import {
|
||||
DeepPartial,
|
||||
EmbeddedScene,
|
||||
@@ -31,6 +32,25 @@ export function setupLoadDashboardMock(rsp: DeepPartial<DashboardDTO>, spy?: jes
|
||||
} as unknown as DashboardLoaderSrv);
|
||||
return loadDashboardMock;
|
||||
}
|
||||
export function setupLoadDashboardMockReject(rsp: DeepPartial<FetchError>, spy?: jest.Mock) {
|
||||
const loadDashboardMock = (spy || jest.fn()).mockRejectedValue(rsp);
|
||||
// disabling type checks since this is a test util
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
setDashboardLoaderSrv({
|
||||
loadDashboard: loadDashboardMock,
|
||||
} as unknown as DashboardLoaderSrv);
|
||||
return loadDashboardMock;
|
||||
}
|
||||
|
||||
export function setupLoadDashboardRuntimeErrorMock() {
|
||||
// disabling type checks since this is a test util
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
setDashboardLoaderSrv({
|
||||
loadDashboard: () => {
|
||||
throw new Error('Runtime error');
|
||||
},
|
||||
} as unknown as DashboardLoaderSrv);
|
||||
}
|
||||
|
||||
export function mockResizeObserver() {
|
||||
window.ResizeObserver = class ResizeObserver {
|
||||
|
||||
@@ -40,7 +40,12 @@ jest.mock('app/features/live/dashboard/dashboardWatcher', () => ({
|
||||
describe('Legacy dashboard API', () => {
|
||||
it('should throw an error if requesting a folder', async () => {
|
||||
const api = new LegacyDashboardAPI();
|
||||
expect(async () => await api.getDashboardDTO('folderUid')).rejects.toThrowError('Dashboard not found');
|
||||
|
||||
await expect(api.getDashboardDTO('folderUid')).rejects.toMatchObject({
|
||||
status: 404,
|
||||
config: { url: `/api/dashboards/uid/folderUid` },
|
||||
data: { message: 'Dashboard not found' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a valid dashboard', async () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AppEvents, UrlQueryMap } from '@grafana/data';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { FetchError, getBackendSrv } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
@@ -33,7 +33,12 @@ export class LegacyDashboardAPI implements DashboardAPI<DashboardDTO, Dashboard>
|
||||
|
||||
if (result.meta.isFolder) {
|
||||
appEvents.emit(AppEvents.alertError, ['Dashboard not found']);
|
||||
throw new Error('Dashboard not found');
|
||||
const fetchError: FetchError = {
|
||||
status: 404,
|
||||
config: { url: `/api/dashboards/uid/${uid}` },
|
||||
data: { message: 'Dashboard not found' },
|
||||
};
|
||||
throw fetchError;
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -133,6 +133,13 @@ describe('v0 dashboard API', () => {
|
||||
expect(result.meta.folderUid).toBe('new-folder');
|
||||
});
|
||||
|
||||
it('throws an error if folder is not found', async () => {
|
||||
jest.spyOn(backendSrv, 'getFolderByUid').mockRejectedValue({ message: 'folder not found', status: 'not-found' });
|
||||
|
||||
const api = new K8sDashboardAPI();
|
||||
await expect(api.getDashboardDTO('test')).rejects.toThrow('Failed to load folder');
|
||||
});
|
||||
|
||||
describe('saveDashboard', () => {
|
||||
beforeEach(() => {
|
||||
locationUtil.initialize({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { locationUtil } from '@grafana/data';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { ScopedResourceClient } from 'app/features/apiserver/client';
|
||||
import {
|
||||
@@ -91,32 +92,46 @@ export class K8sDashboardAPI implements DashboardAPI<DashboardDTO, Dashboard> {
|
||||
}
|
||||
|
||||
async getDashboardDTO(uid: string) {
|
||||
const dash = await this.client.subresource<DashboardWithAccessInfo<DashboardDataDTO>>(uid, 'dto');
|
||||
try {
|
||||
const dash = await this.client.subresource<DashboardWithAccessInfo<DashboardDataDTO>>(uid, 'dto');
|
||||
|
||||
const result: DashboardDTO = {
|
||||
meta: {
|
||||
...dash.access,
|
||||
isNew: false,
|
||||
isFolder: false,
|
||||
uid: dash.metadata.name,
|
||||
k8s: dash.metadata,
|
||||
version: parseInt(dash.metadata.resourceVersion, 10),
|
||||
},
|
||||
dashboard: dash.spec,
|
||||
};
|
||||
const result: DashboardDTO = {
|
||||
meta: {
|
||||
...dash.access,
|
||||
isNew: false,
|
||||
isFolder: false,
|
||||
uid: dash.metadata.name,
|
||||
k8s: dash.metadata,
|
||||
version: parseInt(dash.metadata.resourceVersion, 10),
|
||||
},
|
||||
dashboard: dash.spec,
|
||||
};
|
||||
|
||||
if (dash.metadata.annotations?.[AnnoKeyFolder]) {
|
||||
try {
|
||||
const folder = await backendSrv.getFolderByUid(dash.metadata.annotations[AnnoKeyFolder]);
|
||||
result.meta.folderTitle = folder.title;
|
||||
result.meta.folderUrl = folder.url;
|
||||
result.meta.folderUid = folder.uid;
|
||||
result.meta.folderId = folder.id;
|
||||
} catch (e) {
|
||||
console.error('Failed to load a folder', e);
|
||||
if (dash.metadata.annotations?.[AnnoKeyFolder]) {
|
||||
try {
|
||||
const folder = await backendSrv.getFolderByUid(dash.metadata.annotations[AnnoKeyFolder]);
|
||||
result.meta.folderTitle = folder.title;
|
||||
result.meta.folderUrl = folder.url;
|
||||
result.meta.folderUid = folder.uid;
|
||||
result.meta.folderId = folder.id;
|
||||
} catch (e) {
|
||||
throw new Error('Failed to load folder');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
return result;
|
||||
} catch (e) {
|
||||
const status = getStatusFromError(e);
|
||||
const message = getMessageFromError(e);
|
||||
// Hacking around a bug in k8s api server that returns 500 for not found resources
|
||||
if (message.includes('not found') && status !== 404) {
|
||||
// @ts-expect-error
|
||||
e.status = 404;
|
||||
// @ts-expect-error
|
||||
e.data.message = 'Dashboard not found';
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +75,7 @@ describe('v2 dashboard API', () => {
|
||||
updatedBy: '',
|
||||
});
|
||||
|
||||
const convertToV1 = false;
|
||||
const api = new K8sDashboardV2API(convertToV1);
|
||||
const api = new K8sDashboardV2API(false);
|
||||
// because the API can currently return both DashboardDTO and DashboardWithAccessInfo<DashboardV2Spec> based on the
|
||||
// parameter convertToV1, we need to cast the result to DashboardWithAccessInfo<DashboardV2Spec> to be able to
|
||||
// access
|
||||
@@ -86,6 +85,13 @@ describe('v2 dashboard API', () => {
|
||||
expect(result.metadata.annotations![AnnoKeyFolderUrl]).toBe('/folder/url');
|
||||
expect(result.metadata.annotations![AnnoKeyFolder]).toBe('new-folder');
|
||||
});
|
||||
|
||||
it('throws an error if folder is not found', async () => {
|
||||
jest.spyOn(backendSrv, 'getFolderByUid').mockRejectedValue({ message: 'folder not found', status: 'not-found' });
|
||||
|
||||
const api = new K8sDashboardV2API(false);
|
||||
await expect(api.getDashboardDTO('test')).rejects.toThrow('Failed to load folder');
|
||||
});
|
||||
});
|
||||
|
||||
describe('v2 dashboard API - Save', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { locationUtil, UrlQueryMap } from '@grafana/data';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { ScopedResourceClient } from 'app/features/apiserver/client';
|
||||
import {
|
||||
@@ -37,33 +38,47 @@ export class K8sDashboardV2API
|
||||
}
|
||||
|
||||
async getDashboardDTO(uid: string, params?: UrlQueryMap) {
|
||||
const dashboard = await this.client.subresource<DashboardWithAccessInfo<DashboardV2Spec>>(uid, 'dto');
|
||||
try {
|
||||
const dashboard = await this.client.subresource<DashboardWithAccessInfo<DashboardV2Spec>>(uid, 'dto');
|
||||
|
||||
let result: DashboardWithAccessInfo<DashboardV2Spec> | DashboardDTO | undefined;
|
||||
let result: DashboardWithAccessInfo<DashboardV2Spec> | DashboardDTO | undefined;
|
||||
|
||||
// TODO: For dev purposes only, the conversion should and will happen in the API. This is just to stub v2 api responses.
|
||||
result = ResponseTransformers.ensureV2Response(dashboard);
|
||||
// TODO: For dev purposes only, the conversion should and will happen in the API. This is just to stub v2 api responses.
|
||||
result = ResponseTransformers.ensureV2Response(dashboard);
|
||||
|
||||
// load folder info if available
|
||||
if (result.metadata.annotations && result.metadata.annotations[AnnoKeyFolder]) {
|
||||
try {
|
||||
const folder = await backendSrv.getFolderByUid(result.metadata.annotations[AnnoKeyFolder]);
|
||||
result.metadata.annotations[AnnoKeyFolderTitle] = folder.title;
|
||||
result.metadata.annotations[AnnoKeyFolderUrl] = folder.url;
|
||||
result.metadata.annotations[AnnoKeyFolderId] = folder.id;
|
||||
} catch (e) {
|
||||
console.error('Failed to load a folder', e);
|
||||
// load folder info if available
|
||||
if (result.metadata.annotations && result.metadata.annotations[AnnoKeyFolder]) {
|
||||
try {
|
||||
const folder = await backendSrv.getFolderByUid(result.metadata.annotations[AnnoKeyFolder]);
|
||||
result.metadata.annotations[AnnoKeyFolderTitle] = folder.title;
|
||||
result.metadata.annotations[AnnoKeyFolderUrl] = folder.url;
|
||||
result.metadata.annotations[AnnoKeyFolderId] = folder.id;
|
||||
} catch (e) {
|
||||
throw new Error('Failed to load folder');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Depending on the ui components readiness, we might need to convert the response to v1
|
||||
if (this.convertToV1) {
|
||||
// Always return V1 format
|
||||
result = ResponseTransformers.ensureV1Response(result);
|
||||
// Depending on the ui components readiness, we might need to convert the response to v1
|
||||
if (this.convertToV1) {
|
||||
// Always return V1 format
|
||||
result = ResponseTransformers.ensureV1Response(result);
|
||||
return result;
|
||||
}
|
||||
// return the v2 response
|
||||
return result;
|
||||
} catch (e) {
|
||||
const status = getStatusFromError(e);
|
||||
const message = getMessageFromError(e);
|
||||
// Hacking around a bug in k8s api server that returns 500 for not found resources
|
||||
if (message.includes('not found') && status !== 404) {
|
||||
// @ts-expect-error
|
||||
e.status = 404;
|
||||
// @ts-expect-error
|
||||
e.data.message = 'Dashboard not found';
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
// return the v2 response
|
||||
return result;
|
||||
}
|
||||
|
||||
deleteDashboard(uid: string, showSuccessAlert: boolean): Promise<DeleteDashboardResponse> {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Themeable2, withTheme2 } from '@grafana/ui';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { ScrollRefElement } from 'app/core/components/NativeScrollbar';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
||||
import { GrafanaContext, GrafanaContextType } from 'app/core/context/GrafanaContext';
|
||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
||||
import { getKioskMode } from 'app/core/navigation/kiosk';
|
||||
@@ -26,7 +25,6 @@ import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events';
|
||||
import { cancelVariables, templateVarsChangedInUrl } from '../../variables/state/actions';
|
||||
import { findTemplateVarChanges } from '../../variables/utils';
|
||||
import { DashNav } from '../components/DashNav';
|
||||
import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed';
|
||||
import { DashboardLoading } from '../components/DashboardLoading/DashboardLoading';
|
||||
import { DashboardPrompt } from '../components/DashboardPrompt/DashboardPrompt';
|
||||
import { DashboardSettings } from '../components/DashboardSettings';
|
||||
@@ -41,6 +39,7 @@ import { explicitlyControlledMigrationPanels, autoMigrateAngular } from '../stat
|
||||
import { cleanUpDashboardAndVariables } from '../state/actions';
|
||||
import { initDashboard } from '../state/initDashboard';
|
||||
|
||||
import { DashboardPageError } from './DashboardPageError';
|
||||
import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from './types';
|
||||
|
||||
import 'react-grid-layout/css/styles.css';
|
||||
@@ -355,7 +354,8 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard, initError, queryParams, theme } = this.props;
|
||||
const { dashboard, initError, queryParams, theme, params } = this.props;
|
||||
|
||||
const { editPanel, viewPanel, pageNav, sectionNav } = this.state;
|
||||
const kioskMode = getKioskMode(this.props.queryParams);
|
||||
const styles = getStyles(theme);
|
||||
@@ -367,21 +367,13 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
const inspectPanel = this.getInspectPanel();
|
||||
const showSubMenu = !editPanel && !kioskMode && !this.props.queryParams.editview && dashboard.isSubMenuVisible();
|
||||
|
||||
const showToolbar = kioskMode !== KioskMode.Full && !queryParams.editview;
|
||||
const showToolbar = kioskMode !== KioskMode.Full && !queryParams.editview && !initError;
|
||||
|
||||
const pageClassName = cx({
|
||||
[styles.fullScreenPanel]: Boolean(viewPanel),
|
||||
'page-hidden': Boolean(queryParams.editview || editPanel),
|
||||
});
|
||||
|
||||
if (dashboard.meta.dashboardNotFound) {
|
||||
return (
|
||||
<Page navId="dashboards/browse" layout={PageLayoutType.Canvas} pageNav={{ text: 'Not found' }}>
|
||||
<EntityNotFound entity="Dashboard" />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
const migrationFeatureFlags = new Set([
|
||||
'autoMigrateOldPanels',
|
||||
'autoMigrateGraphPanel',
|
||||
@@ -448,7 +440,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
</header>
|
||||
)}
|
||||
<DashboardPrompt dashboard={dashboard} />
|
||||
{initError && <DashboardFailed />}
|
||||
{initError && <DashboardPageError error={initError.error} type={params.type} />}
|
||||
{showSubMenu && (
|
||||
<section aria-label={selectors.pages.Dashboard.SubMenu.submenu}>
|
||||
<SubMenu dashboard={dashboard} annotations={dashboard.annotations.list} links={dashboard.links} />
|
||||
@@ -463,12 +455,14 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
|
||||
/>
|
||||
)}
|
||||
{showDashboardMigrationNotice && <AngularMigrationNotice dashboardUid={dashboard.uid} />}
|
||||
<DashboardGrid
|
||||
dashboard={dashboard}
|
||||
isEditable={!!dashboard.meta.canEdit}
|
||||
viewPanel={viewPanel}
|
||||
editPanel={editPanel}
|
||||
/>
|
||||
{!initError && (
|
||||
<DashboardGrid
|
||||
dashboard={dashboard}
|
||||
isEditable={!!dashboard.meta.canEdit}
|
||||
viewPanel={viewPanel}
|
||||
editPanel={editPanel}
|
||||
/>
|
||||
)}
|
||||
|
||||
{inspectPanel && <PanelInspector dashboard={dashboard} panel={inspectPanel} />}
|
||||
{queryParams.shareView && (
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { PageLayoutType } from '@grafana/data';
|
||||
import { Alert, Box } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { getMessageFromError, getStatusFromError } from 'app/core/utils/errors';
|
||||
|
||||
export function DashboardPageError({ error, type }: { error: unknown; type?: string }) {
|
||||
const status = getStatusFromError(error);
|
||||
const message = getMessageFromError(error);
|
||||
const entity = type === 'snapshot' ? 'Snapshot' : 'Dashboard';
|
||||
|
||||
return (
|
||||
<Page navId="dashboards/browse" layout={PageLayoutType.Canvas} pageNav={{ text: 'Not found' }}>
|
||||
<Box paddingY={4} display="flex" direction="column" alignItems="center">
|
||||
{status === 404 ? (
|
||||
<EntityNotFound entity={entity} />
|
||||
) : (
|
||||
<Alert
|
||||
title={t('dashboard.errors.failed-to-load', 'Failed to load dashboard')}
|
||||
severity="error"
|
||||
data-testid="dashboard-page-error"
|
||||
>
|
||||
{message}
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -3,13 +3,20 @@ import { useParams } from 'react-router-dom-v5-compat';
|
||||
import { Props } from 'react-virtualized-auto-sizer';
|
||||
import { render } from 'test/test-utils';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import {
|
||||
HOME_DASHBOARD_CACHE_KEY,
|
||||
getDashboardScenePageStateManager,
|
||||
} from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
|
||||
import {
|
||||
setupLoadDashboardMockReject,
|
||||
setupLoadDashboardRuntimeErrorMock,
|
||||
} from 'app/features/dashboard-scene/utils/test-utils';
|
||||
import { DashboardDTO, DashboardRoutes } from 'app/types';
|
||||
|
||||
import { DashboardLoaderSrv, setDashboardLoaderSrv } from '../services/DashboardLoaderSrv';
|
||||
|
||||
import DashboardPageProxy, { DashboardPageProxyProps } from './DashboardPageProxy';
|
||||
|
||||
const dashMock: DashboardDTO = {
|
||||
@@ -63,6 +70,11 @@ jest.mock('@grafana/runtime', () => ({
|
||||
get: jest.fn().mockResolvedValue({}),
|
||||
}),
|
||||
useChromeHeaderHeight: jest.fn(),
|
||||
getBackendSrv: () => {
|
||||
return {
|
||||
get: jest.fn().mockResolvedValue({ dashboard: {}, meta: { url: '' } }),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('react-virtualized-auto-sizer', () => {
|
||||
@@ -75,11 +87,11 @@ jest.mock('react-virtualized-auto-sizer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
jest.mock('app/features/dashboard/api/dashboard_api', () => ({
|
||||
getDashboardAPI: () => ({
|
||||
getDashboardDTO: jest.fn().mockResolvedValue(dashMock),
|
||||
}),
|
||||
}));
|
||||
setDashboardLoaderSrv({
|
||||
loadDashboard: jest.fn().mockResolvedValue(dashMock),
|
||||
// disabling type checks since this is a test util
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
} as unknown as DashboardLoaderSrv);
|
||||
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
@@ -209,4 +221,63 @@ describe('DashboardPageProxy', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('errors rendering', () => {
|
||||
it('should render dashboard not found notice when dashboard... not found', async () => {
|
||||
setupLoadDashboardMockReject({
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
data: {
|
||||
message: 'Dashboard not found',
|
||||
},
|
||||
config: {
|
||||
method: 'GET',
|
||||
url: 'api/dashboards/uid/adfjq9edwm0hsdsa',
|
||||
retry: 0,
|
||||
headers: {
|
||||
'X-Grafana-Org-Id': 1,
|
||||
},
|
||||
hideFromInspector: true,
|
||||
},
|
||||
isHandled: true,
|
||||
});
|
||||
|
||||
setup({ route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' }, uid: 'abc' });
|
||||
|
||||
expect(await screen.findByTestId(selectors.components.EntityNotFound.container)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render error alert for backend errors', async () => {
|
||||
setupLoadDashboardMockReject({
|
||||
status: 500,
|
||||
statusText: 'internal server error',
|
||||
data: {
|
||||
message: 'Internal server error',
|
||||
},
|
||||
config: {
|
||||
method: 'GET',
|
||||
url: 'api/dashboards/uid/adfjq9edwm0hsdsa',
|
||||
retry: 0,
|
||||
headers: {
|
||||
'X-Grafana-Org-Id': 1,
|
||||
},
|
||||
hideFromInspector: true,
|
||||
},
|
||||
isHandled: true,
|
||||
});
|
||||
|
||||
setup({ route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' }, uid: 'abc' });
|
||||
|
||||
expect(await screen.findByTestId('dashboard-page-error')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('dashboard-page-error')).toHaveTextContent('Internal server error');
|
||||
});
|
||||
it('should render error alert for runtime errors', async () => {
|
||||
setupLoadDashboardRuntimeErrorMock();
|
||||
|
||||
setup({ route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' }, uid: 'abc' });
|
||||
|
||||
expect(await screen.findByTestId('dashboard-page-error')).toBeInTheDocument();
|
||||
expect(await screen.findByTestId('dashboard-page-error')).toHaveTextContent('Runtime error');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/
|
||||
import { DashboardRoutes } from 'app/types';
|
||||
|
||||
import DashboardPage, { DashboardPageParams } from './DashboardPage';
|
||||
import { DashboardPageError } from './DashboardPageError';
|
||||
import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from './types';
|
||||
|
||||
export type DashboardPageProxyProps = Omit<
|
||||
@@ -46,9 +47,17 @@ function DashboardPageProxy(props: DashboardPageProxyProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return stateManager.fetchDashboard({ route: props.route.routeName as DashboardRoutes, uid: params.uid ?? '' });
|
||||
return stateManager.fetchDashboard({
|
||||
route: props.route.routeName as DashboardRoutes,
|
||||
uid: params.uid ?? '',
|
||||
type: params.type,
|
||||
});
|
||||
}, [params.uid, props.route.routeName]);
|
||||
|
||||
if (dashboard.error) {
|
||||
return <DashboardPageError error={dashboard.error} />;
|
||||
}
|
||||
|
||||
if (dashboard.loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -260,7 +260,29 @@ describe('PublicDashboardPage', () => {
|
||||
setup(undefined, {
|
||||
dashboard: {
|
||||
...dashboardBase,
|
||||
getModel: () => getTestDashboard(undefined, { publicDashboardEnabled: false, dashboardNotFound: false }),
|
||||
initError: {
|
||||
message: 'Failed to fetch dashboard',
|
||||
error: {
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
data: {
|
||||
statusCode: 403,
|
||||
messageId: 'publicdashboards.notEnabled',
|
||||
message: 'Dashboard paused',
|
||||
},
|
||||
config: {
|
||||
method: 'GET',
|
||||
url: 'api/public/dashboards/4615c835a4e441f09c94fb1b073e6d2e',
|
||||
retry: 0,
|
||||
headers: {
|
||||
'X-Grafana-Org-Id': 1,
|
||||
'X-Grafana-Device-Id': 'da48fad0e58ba327fd7d1e6bd17e9c63',
|
||||
},
|
||||
hideFromInspector: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
getModel: () => getTestDashboard(undefined, { publicDashboardEnabled: false }),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -277,7 +299,28 @@ describe('PublicDashboardPage', () => {
|
||||
setup(undefined, {
|
||||
dashboard: {
|
||||
...dashboardBase,
|
||||
getModel: () => getTestDashboard(undefined, { dashboardNotFound: true }),
|
||||
initError: {
|
||||
message: 'Failed to fetch dashboard',
|
||||
error: {
|
||||
status: 404,
|
||||
statusText: 'Not Found',
|
||||
data: {
|
||||
statusCode: 404,
|
||||
messageId: 'publicdashboards.notFound',
|
||||
message: 'Dashboard not found',
|
||||
},
|
||||
config: {
|
||||
method: 'GET',
|
||||
url: 'api/public/dashboards/ce159fe139fc4d238a7d9c3ae33fb82b',
|
||||
retry: 0,
|
||||
hideFromInspector: true,
|
||||
headers: {
|
||||
'X-Grafana-Device-Id': 'da48fad0e58ba327fd7d1e6bd17e9c63',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
getModel: () => getTestDashboard(undefined, {}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
PublicDashboardPageRouteSearchParams,
|
||||
} from 'app/features/dashboard/containers/types';
|
||||
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
||||
import { useSelector, useDispatch } from 'app/types';
|
||||
import { useSelector, useDispatch, DashboardInitError } from 'app/types';
|
||||
|
||||
import { DashNavTimeControls } from '../components/DashNav/DashNavTimeControls';
|
||||
import { DashboardFailed } from '../components/DashboardLoading/DashboardFailed';
|
||||
@@ -64,6 +64,7 @@ const PublicDashboardPage = (props: Props) => {
|
||||
const prevProps = usePrevious({ ...props, location });
|
||||
const styles = useStyles2(getStyles);
|
||||
const dashboardState = useSelector((store) => store.dashboard);
|
||||
const loadError = dashboardState.initError;
|
||||
const dashboard = dashboardState.getModel();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -96,18 +97,14 @@ const PublicDashboardPage = (props: Props) => {
|
||||
}
|
||||
}, [prevProps, location.search, props.queryParams, dashboard?.timepicker.hidden, accessToken]);
|
||||
|
||||
if (loadError) {
|
||||
return <PublicDashboardPageError error={loadError} />;
|
||||
}
|
||||
|
||||
if (!dashboard) {
|
||||
return <DashboardLoading initPhase={dashboardState.initPhase} />;
|
||||
}
|
||||
|
||||
if (dashboard.meta.publicDashboardEnabled === false) {
|
||||
return <PublicDashboardNotAvailable paused />;
|
||||
}
|
||||
|
||||
if (dashboard.meta.dashboardNotFound) {
|
||||
return <PublicDashboardNotAvailable />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page pageNav={{ text: dashboard.title }} layout={PageLayoutType.Custom} data-testid={selectors.page}>
|
||||
<Toolbar dashboard={dashboard} />
|
||||
@@ -134,3 +131,31 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
});
|
||||
|
||||
export default PublicDashboardPage;
|
||||
|
||||
function PublicDashboardPageError({ error }: { error: DashboardInitError }) {
|
||||
let statusCode: number | undefined;
|
||||
let messageId: string | undefined;
|
||||
|
||||
if (typeof error.error === 'object' && error.error !== null && 'data' in error.error) {
|
||||
const typedError = error.error as { data: { statusCode: number; messageId: string } };
|
||||
statusCode = typedError.data.statusCode;
|
||||
messageId = typedError.data.messageId;
|
||||
}
|
||||
|
||||
const isPublicDashboardPaused = statusCode === 403 && messageId === 'publicdashboards.notEnabled';
|
||||
const isPublicDashboardNotFound = statusCode === 404 && messageId === 'publicdashboards.notFound';
|
||||
const isDashboardNotFound = statusCode === 404 && messageId === 'publicdashboards.dashboardNotFound';
|
||||
|
||||
const publicDashboardEnabled = isPublicDashboardNotFound ? undefined : !isPublicDashboardPaused;
|
||||
const dashboardNotFound = isPublicDashboardNotFound || isDashboardNotFound;
|
||||
|
||||
if (publicDashboardEnabled === false) {
|
||||
return <PublicDashboardNotAvailable paused />;
|
||||
}
|
||||
|
||||
if (dashboardNotFound) {
|
||||
return <PublicDashboardNotAvailable />;
|
||||
}
|
||||
|
||||
return <DashboardFailed initError={error} />;
|
||||
}
|
||||
|
||||
@@ -4,12 +4,10 @@ import moment from 'moment'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
import { AppEvents, dateMath, UrlQueryMap, UrlQueryValue } from '@grafana/data';
|
||||
import { getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
|
||||
import { DashboardV2Spec, defaultDashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha0';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import impressionSrv from 'app/core/services/impression_srv';
|
||||
import { getMessageFromError } from 'app/core/utils/errors';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { AnnoKeyDashboardIsSnapshot, AnnoKeyDashboardNotFound } from 'app/features/apiserver/types';
|
||||
import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { DashboardDTO } from 'app/types';
|
||||
@@ -23,7 +21,6 @@ import { getDashboardSrv } from './DashboardSrv';
|
||||
import { getDashboardSnapshotSrv } from './SnapshotSrv';
|
||||
|
||||
interface DashboardLoaderSrvLike<T> {
|
||||
_dashboardLoadFailed(title: string, snapshot?: boolean): T;
|
||||
loadDashboard(
|
||||
type: UrlQueryValue,
|
||||
slug: string | undefined,
|
||||
@@ -33,7 +30,6 @@ interface DashboardLoaderSrvLike<T> {
|
||||
}
|
||||
|
||||
abstract class DashboardLoaderSrvBase<T> implements DashboardLoaderSrvLike<T> {
|
||||
abstract _dashboardLoadFailed(title: string, snapshot?: boolean): T;
|
||||
abstract loadDashboard(
|
||||
type: UrlQueryValue,
|
||||
slug: string | undefined,
|
||||
@@ -66,7 +62,7 @@ abstract class DashboardLoaderSrvBase<T> implements DashboardLoaderSrvLike<T> {
|
||||
'Script Error',
|
||||
'Please make sure it exists and returns a valid dashboard',
|
||||
]);
|
||||
return this._dashboardLoadFailed('Scripted dashboard');
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -116,22 +112,6 @@ abstract class DashboardLoaderSrvBase<T> implements DashboardLoaderSrvLike<T> {
|
||||
}
|
||||
|
||||
export class DashboardLoaderSrv extends DashboardLoaderSrvBase<DashboardDTO> {
|
||||
_dashboardLoadFailed(title: string, snapshot?: boolean) {
|
||||
snapshot = snapshot || false;
|
||||
return {
|
||||
meta: {
|
||||
canStar: false,
|
||||
isSnapshot: snapshot,
|
||||
canDelete: false,
|
||||
canSave: false,
|
||||
canEdit: false,
|
||||
canShare: false,
|
||||
dashboardNotFound: true,
|
||||
},
|
||||
dashboard: { title, uid: title, schemaVersion: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
loadDashboard(
|
||||
type: UrlQueryValue,
|
||||
slug: string | undefined,
|
||||
@@ -146,38 +126,11 @@ export class DashboardLoaderSrv extends DashboardLoaderSrvBase<DashboardDTO> {
|
||||
// needed for the old architecture
|
||||
// in scenes this is handled through loadSnapshot method
|
||||
} else if (type === 'snapshot' && slug) {
|
||||
promise = getDashboardSnapshotSrv()
|
||||
.getSnapshot(slug)
|
||||
.catch(() => {
|
||||
return this._dashboardLoadFailed('Snapshot not found', true);
|
||||
});
|
||||
promise = getDashboardSnapshotSrv().getSnapshot(slug);
|
||||
} else if (type === 'public' && uid) {
|
||||
promise = backendSrv
|
||||
.getPublicDashboardByUid(uid)
|
||||
.then((result) => {
|
||||
return result;
|
||||
})
|
||||
.catch((e) => {
|
||||
const isPublicDashboardPaused =
|
||||
e.data.statusCode === 403 && e.data.messageId === 'publicdashboards.notEnabled';
|
||||
const isPublicDashboardNotFound =
|
||||
e.data.statusCode === 404 && e.data.messageId === 'publicdashboards.notFound';
|
||||
const isDashboardNotFound =
|
||||
e.data.statusCode === 404 && e.data.messageId === 'publicdashboards.dashboardNotFound';
|
||||
|
||||
const dashboardModel = this._dashboardLoadFailed(
|
||||
isPublicDashboardPaused ? 'Public Dashboard paused' : 'Public Dashboard Not found',
|
||||
true
|
||||
);
|
||||
return {
|
||||
...dashboardModel,
|
||||
meta: {
|
||||
...dashboardModel.meta,
|
||||
publicDashboardEnabled: isPublicDashboardNotFound ? undefined : !isPublicDashboardPaused,
|
||||
dashboardNotFound: isPublicDashboardNotFound || isDashboardNotFound,
|
||||
},
|
||||
};
|
||||
});
|
||||
promise = backendSrv.getPublicDashboardByUid(uid).then((result) => {
|
||||
return result;
|
||||
});
|
||||
} else if (uid) {
|
||||
if (!params) {
|
||||
const cachedDashboard = stateManager.getDashboardFromCache(uid);
|
||||
@@ -188,26 +141,23 @@ export class DashboardLoaderSrv extends DashboardLoaderSrvBase<DashboardDTO> {
|
||||
|
||||
promise = getDashboardAPI()
|
||||
.getDashboardDTO(uid, params)
|
||||
.then((result) => {
|
||||
if (result.meta.isFolder) {
|
||||
appEvents.emit(AppEvents.alertError, ['Dashboard not found']);
|
||||
throw new Error('Dashboard not found');
|
||||
.catch((e) => {
|
||||
console.error('Failed to load dashboard', e);
|
||||
if (isFetchError(e)) {
|
||||
e.isHandled = true;
|
||||
if (e.status === 404) {
|
||||
appEvents.emit(AppEvents.alertError, ['Dashboard not found']);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
})
|
||||
.catch(() => {
|
||||
const dash = this._dashboardLoadFailed('Not found', true);
|
||||
dash.dashboard.uid = '';
|
||||
return dash;
|
||||
|
||||
throw e;
|
||||
});
|
||||
} else {
|
||||
throw new Error('Dashboard uid or slug required');
|
||||
}
|
||||
|
||||
promise.then((result: DashboardDTO) => {
|
||||
if (result.meta.dashboardNotFound !== true) {
|
||||
impressionSrv.addDashboardImpression(result.dashboard.uid);
|
||||
}
|
||||
impressionSrv.addDashboardImpression(result.dashboard.uid);
|
||||
|
||||
return result;
|
||||
});
|
||||
@@ -216,16 +166,10 @@ export class DashboardLoaderSrv extends DashboardLoaderSrvBase<DashboardDTO> {
|
||||
}
|
||||
|
||||
loadSnapshot(slug: string): Promise<DashboardDTO> {
|
||||
const promise = getDashboardSnapshotSrv()
|
||||
.getSnapshot(slug)
|
||||
.catch(() => {
|
||||
return this._dashboardLoadFailed('Snapshot not found', true);
|
||||
});
|
||||
const promise = getDashboardSnapshotSrv().getSnapshot(slug);
|
||||
|
||||
promise.then((result: DashboardDTO) => {
|
||||
if (result.meta.dashboardNotFound !== true) {
|
||||
impressionSrv.addDashboardImpression(result.dashboard.uid);
|
||||
}
|
||||
impressionSrv.addDashboardImpression(result.dashboard.uid);
|
||||
|
||||
return result;
|
||||
});
|
||||
@@ -235,36 +179,6 @@ export class DashboardLoaderSrv extends DashboardLoaderSrvBase<DashboardDTO> {
|
||||
}
|
||||
|
||||
export class DashboardLoaderSrvV2 extends DashboardLoaderSrvBase<DashboardWithAccessInfo<DashboardV2Spec>> {
|
||||
_dashboardLoadFailed(title: string, snapshot?: boolean) {
|
||||
const dashboard: DashboardWithAccessInfo<DashboardV2Spec> = {
|
||||
kind: 'DashboardWithAccessInfo',
|
||||
spec: {
|
||||
...defaultDashboardV2Spec(),
|
||||
title,
|
||||
},
|
||||
access: {
|
||||
canSave: false,
|
||||
canEdit: false,
|
||||
canAdmin: false,
|
||||
canStar: false,
|
||||
canShare: false,
|
||||
canDelete: false,
|
||||
},
|
||||
apiVersion: 'v2alpha1',
|
||||
metadata: {
|
||||
creationTimestamp: '',
|
||||
name: title,
|
||||
namespace: '',
|
||||
resourceVersion: '',
|
||||
annotations: {
|
||||
[AnnoKeyDashboardNotFound]: true,
|
||||
[AnnoKeyDashboardIsSnapshot]: Boolean(snapshot),
|
||||
},
|
||||
},
|
||||
};
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
loadDashboard(
|
||||
type: UrlQueryValue,
|
||||
slug: string | undefined,
|
||||
@@ -277,34 +191,9 @@ export class DashboardLoaderSrvV2 extends DashboardLoaderSrvBase<DashboardWithAc
|
||||
if (type === 'script' && slug) {
|
||||
promise = this.loadScriptedDashboard(slug).then((r) => ResponseTransformers.ensureV2Response(r));
|
||||
} else if (type === 'public' && uid) {
|
||||
promise = backendSrv
|
||||
.getPublicDashboardByUid(uid)
|
||||
.then((result) => {
|
||||
return ResponseTransformers.ensureV2Response(result);
|
||||
})
|
||||
.catch((e) => {
|
||||
const isPublicDashboardPaused =
|
||||
e.data.statusCode === 403 && e.data.messageId === 'publicdashboards.notEnabled';
|
||||
// const isPublicDashboardNotFound =
|
||||
// e.data.statusCode === 404 && e.data.messageId === 'publicdashboards.notFound';
|
||||
// const isDashboardNotFound =
|
||||
// e.data.statusCode === 404 && e.data.messageId === 'publicdashboards.dashboardNotFound';
|
||||
const dashboardModel = this._dashboardLoadFailed(
|
||||
isPublicDashboardPaused ? 'Public Dashboard paused' : 'Public Dashboard Not found',
|
||||
true
|
||||
);
|
||||
|
||||
return dashboardModel;
|
||||
// TODO[schema v2]:
|
||||
// return {
|
||||
// ...dashboardModel,
|
||||
// meta: {
|
||||
// ...dashboardModel.meta,
|
||||
// publicDashboardEnabled: isPublicDashboardNotFound ? undefined : !isPublicDashboardPaused,
|
||||
// dashboardNotFound: isPublicDashboardNotFound || isDashboardNotFound,
|
||||
// },
|
||||
// };
|
||||
});
|
||||
promise = backendSrv.getPublicDashboardByUid(uid).then((result) => {
|
||||
return ResponseTransformers.ensureV2Response(result);
|
||||
});
|
||||
} else if (uid) {
|
||||
if (!params) {
|
||||
const cachedDashboard = stateManager.getDashboardFromCache(uid);
|
||||
@@ -319,21 +208,19 @@ export class DashboardLoaderSrvV2 extends DashboardLoaderSrvBase<DashboardWithAc
|
||||
console.error('Failed to load dashboard', e);
|
||||
if (isFetchError(e)) {
|
||||
e.isHandled = true;
|
||||
if (e.status === 404) {
|
||||
appEvents.emit(AppEvents.alertError, ['Dashboard not found']);
|
||||
}
|
||||
}
|
||||
appEvents.emit(AppEvents.alertError, ['Dashboard not found']);
|
||||
const dash = this._dashboardLoadFailed('Not found', true);
|
||||
|
||||
return dash;
|
||||
throw e;
|
||||
});
|
||||
} else {
|
||||
throw new Error('Dashboard uid or slug required');
|
||||
}
|
||||
|
||||
promise.then((result: DashboardWithAccessInfo<DashboardV2Spec>) => {
|
||||
if (result.metadata.annotations?.[AnnoKeyDashboardNotFound] !== true) {
|
||||
impressionSrv.addDashboardImpression(result.metadata.name);
|
||||
}
|
||||
|
||||
impressionSrv.addDashboardImpression(result.metadata.name);
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -343,16 +230,10 @@ export class DashboardLoaderSrvV2 extends DashboardLoaderSrvBase<DashboardWithAc
|
||||
loadSnapshot(slug: string): Promise<DashboardWithAccessInfo<DashboardV2Spec>> {
|
||||
const promise = getDashboardSnapshotSrv()
|
||||
.getSnapshot(slug)
|
||||
.then((r) => ResponseTransformers.ensureV2Response(r))
|
||||
.catch((e) => {
|
||||
const msg = getMessageFromError(e);
|
||||
throw new Error(`Failed to load snapshot: ${msg}`);
|
||||
});
|
||||
.then((r) => ResponseTransformers.ensureV2Response(r));
|
||||
|
||||
promise.then((result: DashboardWithAccessInfo<DashboardV2Spec>) => {
|
||||
if (result.metadata.annotations?.[AnnoKeyDashboardNotFound] !== true) {
|
||||
impressionSrv.addDashboardImpression(result.metadata.name);
|
||||
}
|
||||
impressionSrv.addDashboardImpression(result.metadata.name);
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
@@ -47,9 +47,13 @@ const legacyDashboardSnapshotSrv: DashboardSnapshotSrv = {
|
||||
getSharingOptions: () => getBackendSrv().get<SnapshotSharingOptions>('/api/snapshot/shared-options'),
|
||||
deleteSnapshot: (key: string) => getBackendSrv().delete('/api/snapshots/' + key),
|
||||
getSnapshot: async (key: string) => {
|
||||
const dto = await getBackendSrv().get<DashboardDTO>('/api/snapshots/' + key);
|
||||
dto.meta.canShare = false;
|
||||
return dto;
|
||||
try {
|
||||
const dto = await getBackendSrv().get<DashboardDTO>('/api/snapshots/' + key);
|
||||
dto.meta.canShare = false;
|
||||
return dto;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ export interface DashboardMeta {
|
||||
hasUnsavedFolderChange?: boolean;
|
||||
annotationsPermissions?: AnnotationsPermissions;
|
||||
publicDashboardEnabled?: boolean;
|
||||
dashboardNotFound?: boolean;
|
||||
isEmbedded?: boolean;
|
||||
isNew?: boolean;
|
||||
version?: number;
|
||||
|
||||
@@ -893,6 +893,9 @@
|
||||
"import-a-dashboard-header": "Import a dashboard",
|
||||
"import-dashboard-button": "Import dashboard"
|
||||
},
|
||||
"errors": {
|
||||
"failed-to-load": "Failed to load dashboard"
|
||||
},
|
||||
"inspect": {
|
||||
"data-tab": "Data",
|
||||
"error-tab": "Error",
|
||||
|
||||
@@ -893,6 +893,9 @@
|
||||
"import-a-dashboard-header": "Ĩmpőřŧ ä đäşĥþőäřđ",
|
||||
"import-dashboard-button": "Ĩmpőřŧ đäşĥþőäřđ"
|
||||
},
|
||||
"errors": {
|
||||
"failed-to-load": "Fäįľęđ ŧő ľőäđ đäşĥþőäřđ"
|
||||
},
|
||||
"inspect": {
|
||||
"data-tab": "Đäŧä",
|
||||
"error-tab": "Ēřřőř",
|
||||
|
||||
Reference in New Issue
Block a user