mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[Dashboard] 404 page (#160213)
Adds a 404 page, and ensures that errors are properly surfaced both if they happen at dashboard container creation time and if they happen when navigating from one dashboard to the next.
This commit is contained in:
parent
7340007718
commit
d0fe5e93b6
18 changed files with 158 additions and 53 deletions
|
@ -91,11 +91,6 @@ export const getPanelAddedSuccessString = (savedObjectName: string) =>
|
|||
},
|
||||
});
|
||||
|
||||
export const getDashboardURL404String = () =>
|
||||
i18n.translate('dashboard.loadingError.dashboardNotFound', {
|
||||
defaultMessage: 'The requested dashboard could not be found.',
|
||||
});
|
||||
|
||||
export const getPanelTooOldErrorString = () =>
|
||||
i18n.translate('dashboard.loadURLError.PanelTooOld', {
|
||||
defaultMessage: 'Cannot load panels from a URL created in a version older than 7.3',
|
||||
|
|
|
@ -30,11 +30,12 @@ import {
|
|||
createSessionRestorationDataProvider,
|
||||
} from './url/search_sessions_integration';
|
||||
import { DashboardAPI, DashboardRenderer } from '..';
|
||||
import { type DashboardEmbedSettings } from './types';
|
||||
import { DASHBOARD_APP_ID } from '../dashboard_constants';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { DashboardTopNav } from './top_nav/dashboard_top_nav';
|
||||
import { AwaitingDashboardAPI } from '../dashboard_container';
|
||||
import { type DashboardEmbedSettings, DashboardRedirect } from './types';
|
||||
import { DashboardRedirect } from '../dashboard_container/types';
|
||||
import { useDashboardMountContext } from './hooks/dashboard_mount_context';
|
||||
import { useDashboardOutcomeValidation } from './hooks/use_dashboard_outcome_validation';
|
||||
import { loadDashboardHistoryLocationState } from './locator/load_dashboard_history_location_state';
|
||||
|
@ -113,9 +114,7 @@ export function DashboardApp({
|
|||
/**
|
||||
* Validate saved object load outcome
|
||||
*/
|
||||
const { validateOutcome, getLegacyConflictWarning } = useDashboardOutcomeValidation({
|
||||
redirectTo,
|
||||
});
|
||||
const { validateOutcome, getLegacyConflictWarning } = useDashboardOutcomeValidation();
|
||||
|
||||
/**
|
||||
* Create options to pass into the dashboard renderer
|
||||
|
@ -202,6 +201,7 @@ export function DashboardApp({
|
|||
|
||||
<DashboardRenderer
|
||||
ref={setDashboardAPI}
|
||||
dashboardRedirect={redirectTo}
|
||||
savedObjectId={savedDashboardId}
|
||||
showPlainSpinner={showPlainSpinner}
|
||||
getCreationOptions={getCreationOptions}
|
||||
|
|
|
@ -28,13 +28,14 @@ import {
|
|||
} from '../dashboard_constants';
|
||||
import { DashboardApp } from './dashboard_app';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { DashboardNoMatch } from './listing_page/dashboard_no_match';
|
||||
import { RedirectToProps } from '../dashboard_container/types';
|
||||
import { createDashboardEditUrl } from '../dashboard_constants';
|
||||
import { DashboardNoMatch } from './listing_page/dashboard_no_match';
|
||||
import { DashboardStart, DashboardStartDependencies } from '../plugin';
|
||||
import { DashboardMountContext } from './hooks/dashboard_mount_context';
|
||||
import { DashboardEmbedSettings, DashboardMountContextProps } from './types';
|
||||
import { DashboardListingPage } from './listing_page/dashboard_listing_page';
|
||||
import { dashboardReadonlyBadge, getDashboardPageTitle } from './_dashboard_app_strings';
|
||||
import { DashboardEmbedSettings, DashboardMountContextProps, RedirectToProps } from './types';
|
||||
|
||||
export const dashboardUrlParams = {
|
||||
showTopMenu: 'show-top-menu',
|
||||
|
|
|
@ -8,18 +8,12 @@
|
|||
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { DashboardRedirect } from '../types';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { createDashboardEditUrl } from '../../dashboard_constants';
|
||||
import { getDashboardURL404String } from '../_dashboard_app_strings';
|
||||
import { useDashboardMountContext } from './dashboard_mount_context';
|
||||
import { LoadDashboardReturn } from '../../services/dashboard_content_management/types';
|
||||
|
||||
export const useDashboardOutcomeValidation = ({
|
||||
redirectTo,
|
||||
}: {
|
||||
redirectTo: DashboardRedirect;
|
||||
}) => {
|
||||
export const useDashboardOutcomeValidation = () => {
|
||||
const [aliasId, setAliasId] = useState<string>();
|
||||
const [outcome, setOutcome] = useState<string>();
|
||||
const [savedObjectId, setSavedObjectId] = useState<string>();
|
||||
|
@ -30,17 +24,11 @@ export const useDashboardOutcomeValidation = ({
|
|||
/**
|
||||
* Unpack dashboard services
|
||||
*/
|
||||
const {
|
||||
notifications: { toasts },
|
||||
screenshotMode,
|
||||
spaces,
|
||||
} = pluginServices.getServices();
|
||||
const { screenshotMode, spaces } = pluginServices.getServices();
|
||||
|
||||
const validateOutcome = useCallback(
|
||||
({ dashboardFound, resolveMeta, dashboardId }: LoadDashboardReturn) => {
|
||||
if (!dashboardFound) {
|
||||
toasts.addDanger(getDashboardURL404String());
|
||||
redirectTo({ destination: 'listing' });
|
||||
return false; // redirected. Stop loading dashboard.
|
||||
}
|
||||
|
||||
|
@ -64,7 +52,7 @@ export const useDashboardOutcomeValidation = ({
|
|||
}
|
||||
return true;
|
||||
},
|
||||
[scopedHistory, redirectTo, screenshotMode, spaces, toasts]
|
||||
[scopedHistory, screenshotMode, spaces]
|
||||
);
|
||||
|
||||
const getLegacyConflictWarning = useMemo(() => {
|
||||
|
|
|
@ -16,9 +16,9 @@ import {
|
|||
DashboardAppNoDataPage,
|
||||
isDashboardAppInNoDataState,
|
||||
} from '../no_data/dashboard_app_no_data';
|
||||
import { DashboardRedirect } from '../types';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { getDashboardBreadcrumb } from '../_dashboard_app_strings';
|
||||
import { DashboardRedirect } from '../../dashboard_container/types';
|
||||
import { getDashboardListItemLink } from './get_dashboard_list_item_link';
|
||||
import { DashboardListing } from '../../dashboard_listing/dashboard_listing';
|
||||
|
||||
|
|
|
@ -26,9 +26,10 @@ import {
|
|||
} from '../_dashboard_app_strings';
|
||||
import { UI_SETTINGS } from '../../../common';
|
||||
import { useDashboardAPI } from '../dashboard_app';
|
||||
import { DashboardEmbedSettings } from '../types';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { useDashboardMenuItems } from './use_dashboard_menu_items';
|
||||
import { DashboardEmbedSettings, DashboardRedirect } from '../types';
|
||||
import { DashboardRedirect } from '../../dashboard_container/types';
|
||||
import { DashboardEditingToolbar } from './dashboard_editing_toolbar';
|
||||
import { useDashboardMountContext } from '../hooks/dashboard_mount_context';
|
||||
import { getFullEditPath, LEGACY_DASHBOARD_APP_ID } from '../../dashboard_constants';
|
||||
|
|
|
@ -12,13 +12,13 @@ import { Dispatch, SetStateAction, useCallback, useMemo, useState } from 'react'
|
|||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { TopNavMenuData } from '@kbn/navigation-plugin/public';
|
||||
|
||||
import { DashboardRedirect } from '../types';
|
||||
import { UI_SETTINGS } from '../../../common';
|
||||
import { useDashboardAPI } from '../dashboard_app';
|
||||
import { topNavStrings } from '../_dashboard_app_strings';
|
||||
import { ShowShareModal } from './share/show_share_modal';
|
||||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { CHANGE_CHECK_DEBOUNCE } from '../../dashboard_constants';
|
||||
import { DashboardRedirect } from '../../dashboard_container/types';
|
||||
import { SaveDashboardReturn } from '../../services/dashboard_content_management/types';
|
||||
import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays';
|
||||
|
||||
|
|
|
@ -8,11 +8,6 @@
|
|||
|
||||
import { AppMountParameters, ScopedHistory } from '@kbn/core-application-browser';
|
||||
|
||||
export type DashboardRedirect = (props: RedirectToProps) => void;
|
||||
export type RedirectToProps =
|
||||
| { destination: 'dashboard'; id?: string; useReplace?: boolean; editMode?: boolean }
|
||||
| { destination: 'listing'; filter?: string; useReplace?: boolean };
|
||||
|
||||
export interface DashboardEmbedSettings {
|
||||
forceHideFilterBar?: boolean;
|
||||
forceShowTopNavMenu?: boolean;
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButtonEmpty } from '@elastic/eui';
|
||||
import { NotFoundPrompt } from '@kbn/shared-ux-prompt-not-found';
|
||||
|
||||
import { DashboardRedirect } from '../types';
|
||||
|
||||
export const Dashboard404Page = ({
|
||||
dashboardRedirect,
|
||||
}: {
|
||||
dashboardRedirect?: DashboardRedirect;
|
||||
}) => {
|
||||
return (
|
||||
<NotFoundPrompt
|
||||
title={i18n.translate('dashboard.renderer.404Title', {
|
||||
defaultMessage: 'Dashboard not found',
|
||||
})}
|
||||
body={i18n.translate('dashboard.renderer.404Body', {
|
||||
defaultMessage:
|
||||
"Sorry, the dashboard you're looking for can't be found. It might have been removed or renamed, or maybe it never existed at all.",
|
||||
})}
|
||||
actions={
|
||||
dashboardRedirect
|
||||
? [
|
||||
<EuiButtonEmpty
|
||||
iconType="arrowLeft"
|
||||
flush="both"
|
||||
onClick={() => dashboardRedirect({ destination: 'listing' })}
|
||||
>
|
||||
{i18n.translate('dashboard.renderer.404Action', {
|
||||
defaultMessage: 'View available dashboards',
|
||||
})}
|
||||
</EuiButtonEmpty>,
|
||||
]
|
||||
: undefined // if dashboard redirect not given, fall back to `go back`.
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -10,6 +10,7 @@ import React from 'react';
|
|||
import { ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithIntl } from '@kbn/test-jest-helpers';
|
||||
import { NotFoundPrompt } from '@kbn/shared-ux-prompt-not-found';
|
||||
|
||||
import { DashboardContainerFactory } from '..';
|
||||
import { DASHBOARD_CONTAINER_TYPE } from '../..';
|
||||
|
@ -17,6 +18,7 @@ import { DashboardRenderer } from './dashboard_renderer';
|
|||
import { pluginServices } from '../../services/plugin_services';
|
||||
import { DashboardContainer } from '../embeddable/dashboard_container';
|
||||
import { DashboardCreationOptions } from '../embeddable/dashboard_container_factory';
|
||||
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
|
||||
|
||||
describe('dashboard renderer', () => {
|
||||
let mockDashboardContainer: DashboardContainer;
|
||||
|
@ -26,7 +28,7 @@ describe('dashboard renderer', () => {
|
|||
mockDashboardContainer = {
|
||||
destroy: jest.fn(),
|
||||
render: jest.fn(),
|
||||
navigateToDashboard: jest.fn(),
|
||||
navigateToDashboard: jest.fn().mockResolvedValue({}),
|
||||
} as unknown as DashboardContainer;
|
||||
mockDashboardFactory = {
|
||||
create: jest.fn().mockReturnValue(mockDashboardContainer),
|
||||
|
@ -162,4 +164,51 @@ describe('dashboard renderer', () => {
|
|||
// instead we should call create on the factory again.
|
||||
expect(mockSuccessFactory.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('renders a 404 page when initial dashboard creation returns a savedObjectNotFound error', async () => {
|
||||
// ensure that the first attempt at creating a dashboard results in a 404
|
||||
const mockErrorEmbeddable = {
|
||||
error: new SavedObjectNotFound('dashboard', 'gat em'),
|
||||
destroy: jest.fn(),
|
||||
render: jest.fn(),
|
||||
} as unknown as DashboardContainer;
|
||||
const mockErrorFactory = {
|
||||
create: jest.fn().mockReturnValue(mockErrorEmbeddable),
|
||||
} as unknown as DashboardContainerFactory;
|
||||
pluginServices.getServices().embeddable.getEmbeddableFactory = jest
|
||||
.fn()
|
||||
.mockReturnValue(mockErrorFactory);
|
||||
|
||||
// render the dashboard - it should run into an error and render the error embeddable.
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = await mountWithIntl(<DashboardRenderer savedObjectId="saved_object_kibanana" />);
|
||||
});
|
||||
await wrapper!.update();
|
||||
|
||||
// The shared UX not found prompt should be rendered.
|
||||
expect(wrapper!.find(NotFoundPrompt).exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
test('renders a 404 page when dashboard navigation returns a savedObjectNotFound error', async () => {
|
||||
mockDashboardContainer.navigateToDashboard = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new SavedObjectNotFound('dashboard', 'gat em'));
|
||||
|
||||
let wrapper: ReactWrapper;
|
||||
await act(async () => {
|
||||
wrapper = await mountWithIntl(<DashboardRenderer savedObjectId="saved_object_kibanana" />);
|
||||
});
|
||||
// The shared UX not found prompt should not be rendered.
|
||||
expect(wrapper!.find(NotFoundPrompt).exists()).toBeFalsy();
|
||||
|
||||
expect(mockDashboardContainer.render).toHaveBeenCalled();
|
||||
await act(async () => {
|
||||
await wrapper.setProps({ savedObjectId: 'saved_object_kibanakiwi' });
|
||||
});
|
||||
await wrapper!.update();
|
||||
|
||||
// The shared UX not found prompt should be rendered.
|
||||
expect(wrapper!.find(NotFoundPrompt).exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ import classNames from 'classnames';
|
|||
import useUnmount from 'react-use/lib/useUnmount';
|
||||
|
||||
import { EuiLoadingElastic, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/common';
|
||||
import { ErrorEmbeddable, isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
import {
|
||||
|
@ -33,23 +34,27 @@ import {
|
|||
DashboardContainerFactory,
|
||||
DashboardContainerFactoryDefinition,
|
||||
} from '../embeddable/dashboard_container_factory';
|
||||
import { DashboardRedirect } from '../types';
|
||||
import { DASHBOARD_CONTAINER_TYPE } from '..';
|
||||
import { DashboardContainerInput } from '../../../common';
|
||||
import type { DashboardContainer } from '../embeddable/dashboard_container';
|
||||
import { Dashboard404Page } from './dashboard_404';
|
||||
|
||||
export interface DashboardRendererProps {
|
||||
savedObjectId?: string;
|
||||
showPlainSpinner?: boolean;
|
||||
dashboardRedirect?: DashboardRedirect;
|
||||
getCreationOptions?: () => Promise<DashboardCreationOptions>;
|
||||
}
|
||||
|
||||
export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRendererProps>(
|
||||
({ savedObjectId, getCreationOptions, showPlainSpinner }, ref) => {
|
||||
({ savedObjectId, getCreationOptions, dashboardRedirect, showPlainSpinner }, ref) => {
|
||||
const dashboardRoot = useRef(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [screenshotMode, setScreenshotMode] = useState(false);
|
||||
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer>();
|
||||
const [fatalError, setFatalError] = useState<ErrorEmbeddable | undefined>();
|
||||
const [dashboardMissing, setDashboardMissing] = useState(false);
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
|
@ -71,19 +76,30 @@ export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRende
|
|||
const id = useMemo(() => uuidv4(), []);
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Here we attempt to build a dashboard or navigate to a new dashboard. Clear all error states
|
||||
* if they exist in case this dashboard loads correctly.
|
||||
*/
|
||||
fatalError?.destroy();
|
||||
setDashboardMissing(false);
|
||||
setFatalError(undefined);
|
||||
|
||||
if (dashboardContainer) {
|
||||
// When a dashboard already exists, don't rebuild it, just set a new id.
|
||||
dashboardContainer.navigateToDashboard(savedObjectId);
|
||||
|
||||
dashboardContainer.navigateToDashboard(savedObjectId).catch((e) => {
|
||||
dashboardContainer?.destroy();
|
||||
setDashboardContainer(undefined);
|
||||
setFatalError(new ErrorEmbeddable(e, { id }));
|
||||
if (e instanceof SavedObjectNotFound) {
|
||||
setDashboardMissing(true);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
let canceled = false;
|
||||
(async () => {
|
||||
fatalError?.destroy();
|
||||
setFatalError(undefined);
|
||||
|
||||
const creationOptions = await getCreationOptions?.();
|
||||
|
||||
// Lazy loading all services is required in this component because it is exported and contributes to the bundle size.
|
||||
|
@ -109,6 +125,9 @@ export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRende
|
|||
|
||||
if (isErrorEmbeddable(container)) {
|
||||
setFatalError(container);
|
||||
if (container.error instanceof SavedObjectNotFound) {
|
||||
setDashboardMissing(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -143,6 +162,7 @@ export const DashboardRenderer = forwardRef<AwaitingDashboardAPI, DashboardRende
|
|||
);
|
||||
|
||||
const renderDashboardContents = () => {
|
||||
if (dashboardMissing) return <Dashboard404Page dashboardRedirect={dashboardRedirect} />;
|
||||
if (fatalError) return fatalError.render();
|
||||
if (loading) return loadingSpinner;
|
||||
return <div ref={dashboardRoot} />;
|
||||
|
|
|
@ -16,6 +16,11 @@ export type DashboardReduxState = ReduxEmbeddableState<
|
|||
DashboardPublicState
|
||||
>;
|
||||
|
||||
export type DashboardRedirect = (props: RedirectToProps) => void;
|
||||
export type RedirectToProps =
|
||||
| { destination: 'dashboard'; id?: string; useReplace?: boolean; editMode?: boolean }
|
||||
| { destination: 'listing'; filter?: string; useReplace?: boolean };
|
||||
|
||||
export type DashboardStateFromSaveModal = Pick<
|
||||
DashboardContainerInput,
|
||||
'title' | 'description' | 'tags' | 'timeRestore' | 'timeRange' | 'refreshInterval'
|
||||
|
|
|
@ -10,6 +10,7 @@ import { has } from 'lodash';
|
|||
|
||||
import { Filter, Query } from '@kbn/es-query';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public';
|
||||
import { cleanFiltersForSerialize } from '@kbn/presentation-util-plugin/public';
|
||||
import { rawControlGroupAttributesToControlGroupInput } from '@kbn/controls-plugin/common';
|
||||
import { parseSearchSourceJSON, injectSearchSourceReferences } from '@kbn/data-plugin/public';
|
||||
|
@ -57,17 +58,22 @@ export const loadDashboardState = async ({
|
|||
/**
|
||||
* Load the saved object from Content Management
|
||||
*/
|
||||
const { item: rawDashboardContent, meta: resolveMeta } = await contentManagement.client.get<
|
||||
DashboardCrudTypes['GetIn'],
|
||||
DashboardCrudTypes['GetOut']
|
||||
>({ contentTypeId: DASHBOARD_CONTENT_ID, id });
|
||||
if (!rawDashboardContent.version) {
|
||||
const { item: rawDashboardContent, meta: resolveMeta } = await contentManagement.client
|
||||
.get<DashboardCrudTypes['GetIn'], DashboardCrudTypes['GetOut']>({
|
||||
contentTypeId: DASHBOARD_CONTENT_ID,
|
||||
id,
|
||||
})
|
||||
.catch((e) => {
|
||||
throw new SavedObjectNotFound(DASHBOARD_CONTENT_ID, id);
|
||||
});
|
||||
if (!rawDashboardContent || !rawDashboardContent.version) {
|
||||
return {
|
||||
dashboardInput: newDashboardState,
|
||||
dashboardFound: false,
|
||||
dashboardId: savedObjectId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject saved object references back into the saved object attributes
|
||||
*/
|
||||
|
|
|
@ -61,7 +61,8 @@
|
|||
"@kbn/object-versioning",
|
||||
"@kbn/core-saved-objects-api-server",
|
||||
"@kbn/content-management-table-list-view",
|
||||
"@kbn/content-management-table-list-view-table"
|
||||
"@kbn/content-management-table-list-view-table",
|
||||
"@kbn/shared-ux-prompt-not-found"
|
||||
],
|
||||
"exclude": ["target/**/*"]
|
||||
}
|
||||
|
|
|
@ -1189,7 +1189,6 @@
|
|||
"dashboard.listing.unsaved.editTitle": "Poursuivre les modifications",
|
||||
"dashboard.listing.unsaved.loading": "Chargement",
|
||||
"dashboard.listing.unsaved.resetTitle": "Réinitialiser les modifications",
|
||||
"dashboard.loadingError.dashboardNotFound": "Le tableau de bord demandé est introuvable.",
|
||||
"dashboard.loadURLError.PanelTooOld": "Impossible de charger les panneaux à partir d'une URL créée dans une version antérieure à 7.3",
|
||||
"dashboard.noMatchRoute.bannerTitleText": "Page introuvable",
|
||||
"dashboard.panel.AddToLibrary": "Enregistrer dans la bibliothèque",
|
||||
|
|
|
@ -1189,7 +1189,6 @@
|
|||
"dashboard.listing.unsaved.editTitle": "編集を続行",
|
||||
"dashboard.listing.unsaved.loading": "読み込み中",
|
||||
"dashboard.listing.unsaved.resetTitle": "変更をリセット",
|
||||
"dashboard.loadingError.dashboardNotFound": "リクエストされたダッシュボードが見つかりませんでした。",
|
||||
"dashboard.loadURLError.PanelTooOld": "7.3より古いバージョンで作成されたURLからはパネルを読み込めません",
|
||||
"dashboard.noMatchRoute.bannerTitleText": "ページが見つかりません",
|
||||
"dashboard.panel.AddToLibrary": "ライブラリに保存",
|
||||
|
|
|
@ -1189,7 +1189,6 @@
|
|||
"dashboard.listing.unsaved.editTitle": "继续编辑",
|
||||
"dashboard.listing.unsaved.loading": "正在加载",
|
||||
"dashboard.listing.unsaved.resetTitle": "重置更改",
|
||||
"dashboard.loadingError.dashboardNotFound": "找不到请求的仪表板。",
|
||||
"dashboard.loadURLError.PanelTooOld": "无法通过在早于 7.3 的版本中创建的 URL 加载面板",
|
||||
"dashboard.noMatchRoute.bannerTitleText": "未找到页面",
|
||||
"dashboard.panel.AddToLibrary": "保存到库",
|
||||
|
|
|
@ -18021,9 +18021,9 @@ inquirer@^8.2.3:
|
|||
wrap-ansi "^7.0.0"
|
||||
|
||||
install-artifact-from-github@^1.3.0:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.2.tgz#1a16d9508e40330523a3017ae0d4713ccc64de82"
|
||||
integrity sha512-yCFcLvqk0yQdxx0uJz4t9Z3adDMLAYrcGYv546uRXCSvxE+GqNYhhz/KmrGcUKGI/gVLR9n/e/zM9jX/+ASMJQ==
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.3.tgz#57d89bacfa0f47d7307fe41b6247cda9f9a8079c"
|
||||
integrity sha512-x79SL0d8WOi1ZjXSTUqqs0GPQZ92YArJAN9O46wgU9wdH2U9ecyyhB9YGDbPe2OLV4ptmt6AZYRQZ2GydQZosQ==
|
||||
|
||||
internal-slot@^1.0.3:
|
||||
version "1.0.3"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue