diff --git a/src/platform/plugins/shared/dashboard/jest_setup.ts b/src/platform/plugins/shared/dashboard/jest_setup.ts index a1f15dfeb3a31..666a836c7334f 100644 --- a/src/platform/plugins/shared/dashboard/jest_setup.ts +++ b/src/platform/plugins/shared/dashboard/jest_setup.ts @@ -24,9 +24,10 @@ setStubLogger(); // Start the kibana services with stubs setStubKibanaServices(); -jest.mock('./public/services/dashboard_backup_service', () => { +jest.mock('./public/services/dashboard_api_services', () => { return { getDashboardBackupService: () => mockDashboardBackupService, + initializeDashboardApiServices: () => jest.fn(), }; }); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.test.ts index 12d7f1c86e7b8..e755aa997ae9d 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.test.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.test.ts @@ -55,7 +55,7 @@ describe('loadDashboardApi', () => { }); // eslint-disable-next-line @typescript-eslint/no-var-requires - require('../../services/dashboard_backup_service').getDashboardBackupService = () => ({ + require('../../services/dashboard_api_services').getDashboardBackupService = () => ({ getState: () => ({ query: lastSavedQuery, }), diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.ts index 7886179e0e87c..0b781ceb218e3 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/load_dashboard_api/load_dashboard_api.ts @@ -12,7 +12,6 @@ import { dashboardClient } from '../../dashboard_client'; import { getPanelSettings } from '../../panel_placement/get_panel_placement_settings'; import { DEFAULT_PANEL_PLACEMENT_SETTINGS } from '../../plugin_constants'; import { getAccessControlClient } from '../../services/access_control_service'; -import { getDashboardBackupService } from '../../services/dashboard_backup_service'; import { coreServices } from '../../services/kibana_services'; import { logger } from '../../services/logger'; import { getLastSavedState } from '../default_dashboard_state'; @@ -22,6 +21,10 @@ import { startQueryPerformanceTracking } from '../performance/query_performance_ import type { DashboardCreationOptions } from '../types'; import { getUserAccessControlData } from './get_user_access_control_data'; import { transformPanels } from './transform_panels'; +import { + getDashboardBackupService, + initializeDashboardApiServices, +} from '../../services/dashboard_api_services'; export async function loadDashboardApi({ getCreationOptions, @@ -65,6 +68,7 @@ export async function loadDashboardApi({ return; } + await initializeDashboardApiServices(); const unsavedChanges = creationOptions?.useSessionStorageIntegration ? getDashboardBackupService().getState(savedObjectId) : undefined; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/save_dashboard.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/save_dashboard.ts index bc3910313947f..9bf1072325b23 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/save_dashboard.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/save_modal/save_dashboard.ts @@ -8,7 +8,7 @@ */ import { i18n } from '@kbn/i18n'; -import { getDashboardBackupService } from '../../services/dashboard_backup_service'; +import { getDashboardBackupService } from '../../services/dashboard_api_services'; import { coreServices } from '../../services/kibana_services'; import { dashboardClient } from '../../dashboard_client'; import type { SaveDashboardProps, SaveDashboardReturn } from './types'; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.test.ts index 5e9ea7e5d4b72..01f9c01bf6f13 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.test.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.test.ts @@ -22,8 +22,6 @@ import type { initializeProjectRoutingManager } from './project_routing_manager' import type { DashboardPanel } from '../../server'; import { getSampleDashboardState } from '../mocks'; -jest.mock('../services/dashboard_backup_service', () => ({})); - const setStateMock = () => {}; const layoutUnsavedChanges$ = new BehaviorSubject<{ panels?: DashboardState['panels'] }>({}); @@ -81,7 +79,7 @@ describe('unsavedChangesManager', () => { layoutUnsavedChanges$.next({}); // eslint-disable-next-line @typescript-eslint/no-var-requires - require('../services/dashboard_backup_service').getDashboardBackupService = () => ({ + require('../services/dashboard_api_services').getDashboardBackupService = () => ({ setState: setBackupStateMock, }); }); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.ts index e80fb228d35e6..afed86e5a1a7e 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.ts @@ -18,10 +18,8 @@ import type { import { of } from 'rxjs'; import type { DashboardState } from '../../common'; -import { - getDashboardBackupService, - type DashboardBackupState, -} from '../services/dashboard_backup_service'; +import { type DashboardBackupState } from '../services/dashboard_backup_service'; +import { getDashboardBackupService } from '../services/dashboard_api_services'; import type { initializeLayoutManager } from './layout_manager'; import type { initializeProjectRoutingManager } from './project_routing_manager'; import type { initializeSettingsManager } from './settings_manager'; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/view_mode_manager.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/view_mode_manager.ts index 362520d88cf97..b7f96cea0f848 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/view_mode_manager.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/view_mode_manager.ts @@ -13,7 +13,7 @@ import { BehaviorSubject } from 'rxjs'; import type { SavedObjectAccessControl } from '@kbn/core-saved-objects-common'; import type { DashboardUser } from './types'; import { getAccessControlClient } from '../services/access_control_service'; -import { getDashboardBackupService } from '../services/dashboard_backup_service'; +import { getDashboardBackupService } from '../services/dashboard_api_services'; import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities'; export function initializeViewModeManager({ diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx index 4a5eb253eaa3c..5626ae8e7ebb3 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/no_data/dashboard_app_no_data.tsx @@ -29,7 +29,7 @@ import { shareService, lensService, } from '../../services/kibana_services'; -import { getDashboardBackupService } from '../../services/dashboard_backup_service'; +import { getDashboardBackupService } from '../../services/dashboard_api_services'; import { dashboardClient } from '../../dashboard_client'; export const DashboardAppNoDataPage = ({ diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/share_options_utils.ts b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/share_options_utils.ts index 9d8b8ea8e3df1..7b9a0282d4225 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/share_options_utils.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/share_options_utils.ts @@ -17,7 +17,7 @@ import type { LocatorPublic } from '@kbn/share-plugin/common'; import { toStoredFilters } from '@kbn/as-code-filters-transforms'; import { topNavStrings } from '../../_dashboard_app_strings'; import type { DashboardLocatorParams } from '../../../../common'; -import { getDashboardBackupService } from '../../../services/dashboard_backup_service'; +import { getDashboardBackupService } from '../../../services/dashboard_api_services'; import { dataService, shareService } from '../../../services/kibana_services'; import { logger } from '../../../services/logger'; import { getDashboardCapabilities } from '../../../utils/get_dashboard_capabilities'; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx index 37dc24587bff9..f70381d229543 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx @@ -9,7 +9,7 @@ import type { Capabilities } from '@kbn/core/public'; import type { DashboardLocatorParams, DashboardState } from '../../../../common/types'; -import { getDashboardBackupService } from '../../../services/dashboard_backup_service'; +import { getDashboardBackupService } from '../../../services/dashboard_api_services'; import { shareService } from '../../../services/kibana_services'; import { showPublicUrlSwitch, ShowShareModal } from './show_share_modal'; import type { AccessControlClient } from '@kbn/content-management-access-control-public'; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index 5287ecf1c4b20..667e44fa5c3c2 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -26,7 +26,7 @@ import { UI_SETTINGS } from '../../../common/constants'; import { useDashboardApi } from '../../dashboard_api/use_dashboard_api'; import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays'; import { openSettingsFlyout } from '../../dashboard_renderer/settings/open_settings_flyout'; -import { getDashboardBackupService } from '../../services/dashboard_backup_service'; +import { getDashboardBackupService } from '../../services/dashboard_api_services'; import type { SaveDashboardReturn } from '../../dashboard_api/save_modal/types'; import { coreServices, shareService, dataService } from '../../services/kibana_services'; import { getDashboardCapabilities } from '../../utils/get_dashboard_capabilities'; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx index 2a30de55c64b9..2872456453284 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx @@ -17,11 +17,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { useCallback, useMemo } from 'react'; - -import { - DASHBOARD_PANELS_UNSAVED_ID, - getDashboardBackupService, -} from '../services/dashboard_backup_service'; +import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_backup_service'; +import { getDashboardBackupService } from '../services/dashboard_api_services'; import { coreServices } from '../services/kibana_services'; import { getDashboardCapabilities } from '../utils/get_dashboard_capabilities'; import { diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.test.tsx index efd9ca1cf0d2a..c2ba730719349 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.test.tsx @@ -13,10 +13,8 @@ import { I18nProvider } from '@kbn/i18n-react'; import { render, waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { - DASHBOARD_PANELS_UNSAVED_ID, - getDashboardBackupService, -} from '../services/dashboard_backup_service'; +import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_backup_service'; +import { getDashboardBackupService } from '../services/dashboard_api_services'; import { coreServices } from '../services/kibana_services'; import type { DashboardUnsavedListingProps } from './dashboard_unsaved_listing'; import { DashboardUnsavedListing } from './dashboard_unsaved_listing'; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.tsx index 15764457e43a4..037e94429ec45 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/dashboard_unsaved_listing.tsx @@ -18,19 +18,17 @@ import { EuiTitle, euiBreakpoint, } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import type { ViewMode } from '@kbn/presentation-publishing'; import { css } from '@emotion/react'; import { useMemoCss } from '@kbn/css-utils/public/use_memo_css'; import type { DashboardState } from '../../server'; -import { - DASHBOARD_PANELS_UNSAVED_ID, - getDashboardBackupService, -} from '../services/dashboard_backup_service'; +import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_backup_service'; import { dashboardUnsavedListingStrings, getNewDashboardTitle } from './_dashboard_listing_strings'; import { confirmDiscardUnsavedChanges } from './confirm_overlays'; import { findService } from '../dashboard_client'; +import { getDashboardBackupService } from '../services/dashboard_api_services'; const unsavedItemStyles = { item: (euiThemeContext: UseEuiTheme) => @@ -82,7 +80,12 @@ const DashboardUnsavedItem = ({
- + @@ -145,7 +148,6 @@ export const DashboardUnsavedListing = ({ refreshUnsavedDashboards, }: DashboardUnsavedListingProps) => { const [items, setItems] = useState({}); - const dashboardBackupService = useMemo(() => getDashboardBackupService(), []); const onOpen = useCallback( (id?: string) => { @@ -157,11 +159,11 @@ export const DashboardUnsavedListing = ({ const onDiscard = useCallback( (id?: string) => { confirmDiscardUnsavedChanges(() => { - dashboardBackupService.clearState(id); + getDashboardBackupService().clearState(id); refreshUnsavedDashboards(); }); }, - [dashboardBackupService, refreshUnsavedDashboards] + [refreshUnsavedDashboards] ); useEffect(() => { @@ -183,7 +185,7 @@ export const DashboardUnsavedListing = ({ hasError = true; if (result.error && result.notFound) { // Save object not found error - dashboardBackupService.clearState(result.id); + getDashboardBackupService().clearState(result.id); } return map; } @@ -202,7 +204,7 @@ export const DashboardUnsavedListing = ({ return () => { canceled = true; }; - }, [dashboardBackupService, refreshUnsavedDashboards, unsavedDashboardIds]); + }, [refreshUnsavedDashboards, unsavedDashboardIds]); return unsavedDashboardIds.length === 0 ? null : ( <> diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx index 1499721d7955f..0ac8b2ff3eb66 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.test.tsx @@ -8,12 +8,11 @@ */ import { renderHook, act } from '@testing-library/react'; - -import { getDashboardBackupService } from '../../services/dashboard_backup_service'; import { coreServices } from '../../services/kibana_services'; import { confirmCreateWithUnsaved } from '../confirm_overlays'; import type { DashboardSavedObjectUserContent } from '../types'; import { useDashboardListingTable } from './use_dashboard_listing_table'; +import { getDashboardBackupService } from '../../services/dashboard_api_services'; const clearStateMock = jest.fn(); const getDashboardUrl = jest.fn(); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx index 57b1938b839b5..d8f488243cfb1 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/hooks/use_dashboard_listing_table.tsx @@ -25,7 +25,6 @@ import { findService, } from '../../dashboard_client'; import { getAccessControlClient } from '../../services/access_control_service'; -import { getDashboardBackupService } from '../../services/dashboard_backup_service'; import { getDashboardRecentlyAccessedService } from '../../services/dashboard_recently_accessed_service'; import { coreServices, savedObjectsTaggingService } from '../../services/kibana_services'; import { logger } from '../../services/logger'; @@ -41,6 +40,7 @@ import { import { confirmCreateWithUnsaved } from '../confirm_overlays'; import { DashboardListingEmptyPrompt } from '../dashboard_listing_empty_prompt'; import type { DashboardSavedObjectUserContent } from '../types'; +import { getDashboardBackupService } from '../../services/dashboard_api_services'; type GetDetailViewLink = TableListViewTableProps['getDetailViewLink']; @@ -90,10 +90,8 @@ export const useDashboardListingTable = ({ const [pageDataTestSubject, setPageDataTestSubject] = useState(); const [hasInitialFetchReturned, setHasInitialFetchReturned] = useState(false); - const dashboardBackupService = useMemo(() => getDashboardBackupService(), []); - const [unsavedDashboardIds, setUnsavedDashboardIds] = useState( - dashboardBackupService.getDashboardIdsWithUnsavedChanges() + getDashboardBackupService().getDashboardIdsWithUnsavedChanges() ); const accessControlClient = getAccessControlClient(); @@ -102,15 +100,15 @@ export const useDashboardListingTable = ({ const initialPageSize = coreServices.uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING); const createItem = useCallback(() => { - if (useSessionStorageIntegration && dashboardBackupService.dashboardHasUnsavedEdits()) { + if (useSessionStorageIntegration && getDashboardBackupService().dashboardHasUnsavedEdits()) { confirmCreateWithUnsaved(() => { - dashboardBackupService.clearState(); + getDashboardBackupService().clearState(); goToDashboard(); }, goToDashboard); return; } goToDashboard(); - }, [dashboardBackupService, goToDashboard, useSessionStorageIntegration]); + }, [goToDashboard, useSessionStorageIntegration]); const updateItemMeta = useCallback( async ({ id, ...updatedState }: Parameters['onSave']>[0]) => { @@ -123,9 +121,9 @@ export const useDashboardListingTable = ({ ...updatedState, }); - setUnsavedDashboardIds(dashboardBackupService.getDashboardIdsWithUnsavedChanges()); + setUnsavedDashboardIds(getDashboardBackupService().getDashboardIdsWithUnsavedChanges()); }, - [dashboardBackupService] + [] ); const contentEditorValidators: OpenContentEditorParams['customValidators'] = useMemo( @@ -263,35 +261,32 @@ export const useDashboardListingTable = ({ [listingLimit, accessControlClient] ); - const deleteItems = useCallback( - async (dashboardsToDelete: Array<{ id: string }>) => { - try { - const deleteStartTime = window.performance.now(); + const deleteItems = useCallback(async (dashboardsToDelete: Array<{ id: string }>) => { + try { + const deleteStartTime = window.performance.now(); - await asyncMap(dashboardsToDelete, async ({ id }) => { - await dashboardClient.delete(id); - dashboardBackupService.clearState(id); - }); + await asyncMap(dashboardsToDelete, async ({ id }) => { + await dashboardClient.delete(id); + getDashboardBackupService().clearState(id); + }); - const deleteDuration = window.performance.now() - deleteStartTime; - reportPerformanceMetricEvent(coreServices.analytics, { - eventName: SAVED_OBJECT_DELETE_TIME, - duration: deleteDuration, - meta: { - saved_object_type: DASHBOARD_SAVED_OBJECT_TYPE, - total: dashboardsToDelete.length, - }, - }); - } catch (error) { - coreServices.notifications.toasts.addError(error, { - title: dashboardListingErrorStrings.getErrorDeletingDashboardToast(), - }); - } + const deleteDuration = window.performance.now() - deleteStartTime; + reportPerformanceMetricEvent(coreServices.analytics, { + eventName: SAVED_OBJECT_DELETE_TIME, + duration: deleteDuration, + meta: { + saved_object_type: DASHBOARD_SAVED_OBJECT_TYPE, + total: dashboardsToDelete.length, + }, + }); + } catch (error) { + coreServices.notifications.toasts.addError(error, { + title: dashboardListingErrorStrings.getErrorDeletingDashboardToast(), + }); + } - setUnsavedDashboardIds(dashboardBackupService.getDashboardIdsWithUnsavedChanges()); - }, - [dashboardBackupService] - ); + setUnsavedDashboardIds(getDashboardBackupService().getDashboardIdsWithUnsavedChanges()); + }, []); const editItem = useCallback( ({ id }: { id: string | undefined }) => goToDashboard(id, 'edit'), diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_listing/index.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_listing/index.tsx index d9f7e7b2e5796..753e69e249604 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_listing/index.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_listing/index.tsx @@ -18,10 +18,11 @@ const ListingTableLoadingIndicator = () => { }; const LazyDashboardListing = React.lazy(async () => { - const [{ DashboardListingTable }] = await Promise.all([ + const [{ DashboardListingTable, initializeDashboardApiServices }] = await Promise.all([ import('../dashboard_renderer/dashboard_module'), untilPluginStartServicesReady(), ]); + await initializeDashboardApiServices(); return { default: DashboardListingTable }; }); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/dashboard_module.ts b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/dashboard_module.ts index 941380f3119e3..d2caed58260b2 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/dashboard_module.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/dashboard_module.ts @@ -20,3 +20,4 @@ export { UnlinkFromLibraryAction } from '../dashboard_actions/library_unlink_act export { CopyToDashboardAction } from '../dashboard_actions/copy_to_dashboard_action'; export { AddSectionAction } from '../dashboard_actions/add_section_action'; export { dashboardDrilldown } from '../dashboard_drilldown/dashboard_drilldown'; +export { initializeDashboardApiServices } from '../services/dashboard_api_services'; diff --git a/src/platform/plugins/shared/dashboard/public/plugin.tsx b/src/platform/plugins/shared/dashboard/public/plugin.tsx index fa20e5395d10b..47b918a9adb96 100644 --- a/src/platform/plugins/shared/dashboard/public/plugin.tsx +++ b/src/platform/plugins/shared/dashboard/public/plugin.tsx @@ -73,7 +73,7 @@ import { LANDING_PAGE_PATH, SEARCH_SESSION_ID, } from '../common/page_bundle_constants'; -import { setKibanaServices, untilPluginStartServicesReady } from './services/kibana_services'; +import { untilPluginStartServicesReady, setKibanaServices } from './services/kibana_services'; import { setLogger } from './services/logger'; import { registerActions } from './dashboard_actions/register_actions'; import { setupUrlForwarding } from './dashboard_app/url/setup_url_forwarding'; @@ -256,14 +256,17 @@ export class DashboardPlugin performance.mark(DASHBOARD_DURATION_START_MARK); this.currentHistory = params.history; params.element.classList.add(APP_WRAPPER_CLASS); - const [{ mountApp }] = await Promise.all([ + const [{ mountApp }, { initializeDashboardApiServices }] = await Promise.all([ import('./dashboard_app/dashboard_router'), import('./dashboard_renderer/dashboard_module'), untilPluginStartServicesReady(), ]); appMounted(); - const [coreStart] = await core.getStartServices(); + const [[coreStart], _] = await Promise.all([ + core.getStartServices(), + initializeDashboardApiServices(), + ]); const mountContext: DashboardMountContextProps = { restorePreviousUrl, diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_api_services.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_api_services.ts new file mode 100644 index 0000000000000..d62177dd13884 --- /dev/null +++ b/src/platform/plugins/shared/dashboard/public/services/dashboard_api_services.ts @@ -0,0 +1,37 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { + type DashboardBackupService, + createDashboardBackupService, +} from './dashboard_backup_service'; +import { spacesService } from './kibana_services'; + +let backupService: DashboardBackupService | undefined; +let servicesAvailablePromise: Promise | undefined; + +export const getDashboardBackupService = () => { + if (!backupService) + throw new Error( + 'Ensure initialize Dashboard app services is called before trying to access one of its services' + ); + return backupService; +}; + +/** + * Initializes Dashboard API service singletons if they haven't been initialized already. + */ +export const initializeDashboardApiServices = async () => { + if (backupService) return; + if (servicesAvailablePromise) return servicesAvailablePromise; + servicesAvailablePromise = (async () => { + backupService = await createDashboardBackupService(spacesService); + })(); + return servicesAvailablePromise; +}; diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_backup_service.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_backup_service.ts index 32c9449f2b134..400d299ba6680 100644 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_backup_service.ts +++ b/src/platform/plugins/shared/dashboard/public/services/dashboard_backup_service.ts @@ -7,34 +7,23 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { isEqual } from 'lodash'; -import { firstValueFrom } from 'rxjs'; - -import { i18n } from '@kbn/i18n'; import { Storage } from '@kbn/kibana-utils-plugin/public'; -import { set } from '@kbn/safer-lodash-set'; - import type { ViewMode } from '@kbn/presentation-publishing'; -import { coreServices, spacesService } from './kibana_services'; +import { set } from '@kbn/safer-lodash-set'; +import type { SpacesApi } from '@kbn/spaces-plugin/public'; +import { isEqual } from 'lodash'; +import { firstValueFrom } from 'rxjs'; import type { DashboardState } from '../../common'; export const DASHBOARD_PANELS_UNSAVED_ID = 'unsavedDashboard'; const DASHBOARD_VIEWMODE_LOCAL_KEY = 'dashboardViewMode'; - -// this key is named `panels` for BWC reasons, but actually contains the entire dashboard state const DASHBOARD_STATE_SESSION_KEY = 'dashboardStateManagerPanels'; -const getPanelsGetError = (message: string) => - i18n.translate('dashboard.panelStorageError.getError', { - defaultMessage: 'Error encountered while fetching unsaved changes: {message}', - values: { message }, - }); - export type DashboardBackupState = Partial & { viewMode?: ViewMode; }; -interface DashboardBackupServiceType { +export interface DashboardBackupService { clearState: (id?: string) => void; getState: (id: string | undefined) => DashboardBackupState | undefined; setState: (id: string | undefined, backupState: DashboardBackupState) => void; @@ -44,129 +33,6 @@ interface DashboardBackupServiceType { dashboardHasUnsavedEdits: (id?: string) => boolean; } -class DashboardBackupService implements DashboardBackupServiceType { - private activeSpaceId: string; - private sessionStorage: Storage; - private localStorage: Storage; - private prevDashboardIdsWithUnsavedChanges: string[] = []; - - constructor() { - this.sessionStorage = new Storage(sessionStorage); - this.localStorage = new Storage(localStorage); - - this.activeSpaceId = 'default'; - if (spacesService) { - firstValueFrom(spacesService.getActiveSpace$()).then((space) => { - this.activeSpaceId = space.id; - }); - } - } - - public getViewMode = (): ViewMode => { - return this.localStorage.get(DASHBOARD_VIEWMODE_LOCAL_KEY) ?? 'view'; - }; - - public storeViewMode = (viewMode: ViewMode) => { - try { - this.localStorage.set(DASHBOARD_VIEWMODE_LOCAL_KEY, viewMode); - } catch (e) { - coreServices.notifications.toasts.addDanger({ - title: i18n.translate('dashboard.viewmodeBackup.error', { - defaultMessage: 'Error encountered while backing up view mode: {message}', - values: { message: e.message }, - }), - 'data-test-subj': 'dashboardViewmodeBackupFailure', - }); - } - }; - - public clearState(id = DASHBOARD_PANELS_UNSAVED_ID) { - try { - const allSpaces = this.sessionStorage.get(DASHBOARD_STATE_SESSION_KEY) ?? {}; - const dashboards = this.getDashboards(); - if (dashboards[id]) { - delete dashboards[id]; - this.sessionStorage.set(DASHBOARD_STATE_SESSION_KEY, { - ...allSpaces, - [this.activeSpaceId]: dashboards, - }); - } - } catch (e) { - coreServices.notifications.toasts.addDanger({ - title: i18n.translate('dashboard.panelStorageError.clearError', { - defaultMessage: 'Error encountered while clearing unsaved changes: {message}', - values: { message: e.message }, - }), - 'data-test-subj': 'dashboardPanelsClearFailure', - }); - } - } - - public getState(id = DASHBOARD_PANELS_UNSAVED_ID) { - try { - const dashboards = this.getDashboards(); - return dashboards[id]; - } catch (e) { - coreServices.notifications.toasts.addDanger({ - title: getPanelsGetError(e.message), - 'data-test-subj': 'dashboardPanelsGetFailure', - }); - } - } - - public setState(id = DASHBOARD_PANELS_UNSAVED_ID, backupState: DashboardBackupState) { - try { - const allSpaces = this.sessionStorage.get(DASHBOARD_STATE_SESSION_KEY) ?? {}; - set(allSpaces, [this.activeSpaceId, id], backupState); - this.sessionStorage.set(DASHBOARD_STATE_SESSION_KEY, allSpaces); - } catch (e) { - coreServices.notifications.toasts.addDanger({ - title: i18n.translate('dashboard.panelStorageError.setError', { - defaultMessage: 'Error encountered while setting unsaved changes: {message}', - values: { message: e.message }, - }), - 'data-test-subj': 'dashboardPanelsSetFailure', - }); - } - } - - public getDashboardIdsWithUnsavedChanges() { - try { - const dashboards = this.getDashboards(); - - const dashboardIdsWithUnsavedChanges = Object.keys(dashboards).filter((dashboardId) => { - return hasUnsavedEdits(dashboards[dashboardId]); - }); - - /** - * Because we are storing these unsaved dashboard IDs in React component state, we only want things to be re-rendered - * if the **contents** change, not if the array reference changes - */ - if (isEqual(this.prevDashboardIdsWithUnsavedChanges, dashboardIdsWithUnsavedChanges)) { - return this.prevDashboardIdsWithUnsavedChanges; - } - - this.prevDashboardIdsWithUnsavedChanges = dashboardIdsWithUnsavedChanges; - return dashboardIdsWithUnsavedChanges; - } catch (e) { - coreServices.notifications.toasts.addDanger({ - title: getPanelsGetError(e.message), - 'data-test-subj': 'dashboardPanelsGetFailure', - }); - return []; - } - } - - public dashboardHasUnsavedEdits(id = DASHBOARD_PANELS_UNSAVED_ID) { - const dashboards = this.getDashboards(); - return hasUnsavedEdits(dashboards[id]); - } - - private getDashboards(): { [key: string]: DashboardBackupState } { - return this.sessionStorage.get(DASHBOARD_STATE_SESSION_KEY)?.[this.activeSpaceId] ?? {}; - } -} - function hasUnsavedEdits(backupState?: DashboardBackupState) { return backupState ? backupState.viewMode === 'edit' && @@ -176,11 +42,80 @@ function hasUnsavedEdits(backupState?: DashboardBackupState) { : false; } -let dashboardBackupService: DashboardBackupService; +export const createDashboardBackupService = async ( + spacesService?: SpacesApi +): Promise => { + const sessionStore = new Storage(sessionStorage); + const localStore = new Storage(localStorage); + + const activeSpaceId = await (async () => { + if (spacesService) return (await firstValueFrom(spacesService.getActiveSpace$())).id; + return 'default'; + })(); + + let dashboardIDsWithUnsavedChanges: string[] = []; + const getUnsavedDashboardChanges = (): { [key: string]: DashboardBackupState } => + sessionStore.get(DASHBOARD_STATE_SESSION_KEY)?.[activeSpaceId] ?? {}; + + /** + * The backup service is a non path-critical service that interacts with browser capabilities. + * There is potential for errors, and when an error is encountered we want to swallow the error + * semi-silently and log a warning instead. + */ + const warnOnErrors = ( + attemptFunction: () => ReturnType + ): ReturnType | undefined => { + try { + return attemptFunction(); + } catch (e) { + // eslint-disable-next-line no-console + console.warn('Error encountered in Dashboard backup service', e); + } + }; -export const getDashboardBackupService = () => { - if (!dashboardBackupService) { - dashboardBackupService = new DashboardBackupService(); - } - return dashboardBackupService; + return { + getViewMode: () => localStore.get(DASHBOARD_VIEWMODE_LOCAL_KEY) ?? 'view', + storeViewMode: (viewMode: ViewMode) => { + warnOnErrors(() => localStore.set(DASHBOARD_VIEWMODE_LOCAL_KEY, viewMode)); + }, + clearState: (dashboardId = DASHBOARD_PANELS_UNSAVED_ID) => { + warnOnErrors(() => { + const allSpaces = sessionStore.get(DASHBOARD_STATE_SESSION_KEY) ?? {}; + const dashboards = getUnsavedDashboardChanges(); + if (dashboards[dashboardId]) { + delete dashboards[dashboardId]; + sessionStore.set(DASHBOARD_STATE_SESSION_KEY, { + ...allSpaces, + [activeSpaceId]: dashboards, + }); + } + }); + }, + getState: (dashboardId = DASHBOARD_PANELS_UNSAVED_ID) => { + return warnOnErrors(() => getUnsavedDashboardChanges()[dashboardId]); + }, + setState: (dashboardId = DASHBOARD_PANELS_UNSAVED_ID, backupState: DashboardBackupState) => { + return warnOnErrors(() => { + const allSpaces = sessionStore.get(DASHBOARD_STATE_SESSION_KEY) ?? {}; + set(allSpaces, [activeSpaceId, dashboardId], backupState); + sessionStore.set(DASHBOARD_STATE_SESSION_KEY, allSpaces); + }); + }, + getDashboardIdsWithUnsavedChanges: () => { + return ( + warnOnErrors(() => { + const unsavedDashboards = getUnsavedDashboardChanges(); + const nextDashboardIdsWithUnsavedChanges = Object.keys(unsavedDashboards).filter((id) => + hasUnsavedEdits(unsavedDashboards[id]) + ); + if (!isEqual(nextDashboardIdsWithUnsavedChanges, dashboardIDsWithUnsavedChanges)) { + dashboardIDsWithUnsavedChanges = nextDashboardIdsWithUnsavedChanges; + } + return dashboardIDsWithUnsavedChanges; + }) ?? [] + ); + }, + dashboardHasUnsavedEdits: (id = DASHBOARD_PANELS_UNSAVED_ID) => + warnOnErrors(() => hasUnsavedEdits(getUnsavedDashboardChanges()[id])) ?? false, + }; }; diff --git a/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts b/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts index 7411dda44da87..a6854bb8513dc 100644 --- a/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts +++ b/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts @@ -8,17 +8,17 @@ */ import { BehaviorSubject } from 'rxjs'; - import type { ContentManagementPublicStart } from '@kbn/content-management-plugin/public'; import type { CoreStart } from '@kbn/core/public'; +import type { CPSPluginStart } from '@kbn/cps/public'; import type { DataPublicPluginStart } from '@kbn/data-plugin/public'; import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public'; import type { EmbeddableStart } from '@kbn/embeddable-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public/plugin'; +import type { LensPublicStart } from '@kbn/lens-plugin/public'; import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public'; import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public'; import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; -import type { LensPublicStart } from '@kbn/lens-plugin/public'; import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public'; @@ -26,11 +26,9 @@ import type { ServerlessPluginStart } from '@kbn/serverless/public'; import type { SharePluginStart } from '@kbn/share-plugin/public'; import type { SpacesApi } from '@kbn/spaces-plugin/public'; import type { UiActionsPublicStart } from '@kbn/ui-actions-plugin/public/plugin'; +import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { UrlForwardingStart } from '@kbn/url-forwarding-plugin/public'; import type { UsageCollectionStart } from '@kbn/usage-collection-plugin/public'; -import type { CPSPluginStart } from '@kbn/cps/public'; -import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; - import type { DashboardStartDependencies } from '../plugin'; export let coreServices: CoreStart; @@ -57,6 +55,9 @@ export let usageCollectionService: UsageCollectionStart | undefined; const servicesReady$ = new BehaviorSubject(false); +/** + * Allows module-level access to all of the Dashboard plugin's dependencies. + */ export const setKibanaServices = (kibanaCore: CoreStart, deps: DashboardStartDependencies) => { coreServices = kibanaCore; cpsService = deps.cps; diff --git a/x-pack/platform/plugins/private/translations/translations/de-DE.json b/x-pack/platform/plugins/private/translations/translations/de-DE.json index 8d4b1e32b128a..e927fab08ac07 100644 --- a/x-pack/platform/plugins/private/translations/translations/de-DE.json +++ b/x-pack/platform/plugins/private/translations/translations/de-DE.json @@ -1457,9 +1457,6 @@ "dashboard.panel.unlinkFromLibrary.failureMessage": "Beim Aufheben der Verknüpfung von {panelTitle} aus der Bibliothek ist ein Fehler aufgetreten.", "dashboard.panel.unlinkFromLibrary.successMessage": "Panel {panelTitle} ist nicht mehr mit der Bibliothek verbunden.", "dashboard.panelPlacement.unknownStrategyError": "Unbekannte Strategie zur Panelplatzierung: {strategy}", - "dashboard.panelStorageError.clearError": "Beim Löschen ungespeicherter Änderungen ist ein Fehler aufgetreten: {message}", - "dashboard.panelStorageError.getError": "Fehler beim Abrufen der ungespeicherten Änderungen: {message}", - "dashboard.panelStorageError.setError": "Beim Festlegen ungespeicherter Änderungen ist ein Fehler aufgetreten: {message}", "dashboard.renderer.404Action": "Verfügbare Dashboards anzeigen", "dashboard.renderer.404Body": "Leider kann das gesuchte Dashboard nicht gefunden werden. Es könnte entfernt oder umbenannt worden sein, oder vielleicht hat es nie existiert.", "dashboard.renderer.404Title": "Dashboard nicht gefunden", @@ -1507,7 +1504,6 @@ "dashboard.topNave.viewModeInteractiveSaveConfigDescription": "Erstellen Sie eine Kopie Ihres Dashboards.", "dashboard.unsavedChangesBadge": "Nicht gespeicherte Änderungen", "dashboard.unsavedChangesBadgeToolTipContent": "Sie haben ungespeicherte Änderungen in diesem Dashboard. Um dieses Label zu entfernen, speichern Sie das Dashboard.", - "dashboard.viewmodeBackup.error": "Fehler beim Sichern des Ansichtsmodus: {message}", "data.advancedSettings.autocompleteIgnoreTimerange": "Zeitbereich verwenden", "data.advancedSettings.autocompleteIgnoreTimerangeText": "Deaktivieren Sie diese Eigenschaften, um Vorschläge für die automatische Vervollständigung aus Ihren gesamten Datensätzen und nicht aus dem aktuellen Zeitbereich zu erhalten. {learnMoreLink}", "data.advancedSettings.autocompleteValueSuggestionMethod": "Methode für automatische Vervollständigung mit Vorschlägen", diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index af63f5ded18ef..22023e2c6f7b6 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -1476,9 +1476,6 @@ "dashboard.panel.unlinkFromLibrary.failureMessage": "Une erreur s'est produite lors de la dissociation du panneau {panelTitle} de la bibliothèque.", "dashboard.panel.unlinkFromLibrary.successMessage": "Le panneau {panelTitle} n'est plus connecté à la bibliothèque.", "dashboard.panelPlacement.unknownStrategyError": "Stratégie de placement de panneau inconnue : {strategy}", - "dashboard.panelStorageError.clearError": "Une erreur s'est produite lors de la suppression des modifications non enregistrées : {message}.", - "dashboard.panelStorageError.getError": "Une erreur s'est produite lors de la récupération des modifications non enregistrées : {message}.", - "dashboard.panelStorageError.setError": "Une erreur s'est produite lors de la définition des modifications non enregistrées : {message}.", "dashboard.renderer.404Action": "Voir les tableaux de bord disponibles", "dashboard.renderer.404Body": "Désolé, le tableau de bord que vous recherchez est introuvable. Elle a peut-être été retirée ou renommée, ou peut-être qu'elle n'a jamais existé.", "dashboard.renderer.404Title": "Tableau de bord introuvable", @@ -1526,7 +1523,6 @@ "dashboard.topNave.viewModeInteractiveSaveConfigDescription": "Créer une copie du tableau de bord", "dashboard.unsavedChangesBadge": "Modifications non enregistrées", "dashboard.unsavedChangesBadgeToolTipContent": "Vous avez des modifications non enregistrées dans ce tableau de bord. Pour supprimer cette étiquette, enregistrez le tableau de bord.", - "dashboard.viewmodeBackup.error": "Une erreur s'est produite lors de la sauvegarde du mode d'affichage : {message}", "data.advancedSettings.autocompleteIgnoreTimerange": "Utiliser la plage temporelle", "data.advancedSettings.autocompleteIgnoreTimerangeText": "Désactivez cette propriété pour obtenir des suggestions de saisie semi-automatique depuis l’intégralité de l’ensemble de données plutôt que depuis la plage temporelle définie. {learnMoreLink}", "data.advancedSettings.autocompleteValueSuggestionMethod": "Méthode de suggestion de saisie semi-automatique", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 2d6c0c4916a9c..1693afea4ebf3 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -1477,9 +1477,6 @@ "dashboard.panel.unlinkFromLibrary.failureMessage": "\"{panelTitle}\"をライブラリからリンク解除しているときにエラーが発生しました。", "dashboard.panel.unlinkFromLibrary.successMessage": "パネル\"{panelTitle}\"はライブラリに接続されていません。", "dashboard.panelPlacement.unknownStrategyError": "不明なパネル配置ストラテジ:{strategy}", - "dashboard.panelStorageError.clearError": "保存されていない変更の消去中にエラーが発生しました。{message}", - "dashboard.panelStorageError.getError": "保存されていない変更の取得中にエラーが発生しました。{message}", - "dashboard.panelStorageError.setError": "保存されていない変更の設定中にエラーが発生しました。{message}", "dashboard.renderer.404Action": "使用可能なダッシュボードを表示", "dashboard.renderer.404Body": "申し訳ございません。お探しのダッシュボードは見つかりませんでした。削除または名前変更されたか、そもそも存在していなかった可能性があります。", "dashboard.renderer.404Title": "ダッシュボードが見つかりません", @@ -1527,7 +1524,6 @@ "dashboard.topNave.viewModeInteractiveSaveConfigDescription": "ダッシュボードのコピーを作成します", "dashboard.unsavedChangesBadge": "保存されていない変更", "dashboard.unsavedChangesBadgeToolTipContent": "このダッシュボードには保存されていない変更があります。このラベルを削除するには、ダッシュボードを保存します。", - "dashboard.viewmodeBackup.error": "表示モードのバックアップ中にエラーが発生しました:{message}", "data.advancedSettings.autocompleteIgnoreTimerange": "時間範囲を使用", "data.advancedSettings.autocompleteIgnoreTimerangeText": "このプロパティを無効にすると、現在の時間範囲からではなく、データセットからオートコンプリートの候補を取得します。{learnMoreLink}", "data.advancedSettings.autocompleteValueSuggestionMethod": "自動入力値候補の提案方法", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index 4e53ef225ee5a..9d3eed03e0c74 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -1471,9 +1471,6 @@ "dashboard.panel.unlinkFromLibrary.failureMessage": "从该库中取消链接 {panelTitle} 时出错。", "dashboard.panel.unlinkFromLibrary.successMessage": "面板 {panelTitle} 不再与该库连接。", "dashboard.panelPlacement.unknownStrategyError": "未知面板位置策略:{strategy}", - "dashboard.panelStorageError.clearError": "清除未保存更改时遇到错误:{message}", - "dashboard.panelStorageError.getError": "获取未保存更改时遇到错误:{message}", - "dashboard.panelStorageError.setError": "设置未保存更改时遇到错误:{message}", "dashboard.renderer.404Action": "查看可用仪表板", "dashboard.renderer.404Body": "抱歉,找不到您要查找的仪表板。该页面可能已移除、重命名,或可能根本不存在。", "dashboard.renderer.404Title": "找不到仪表板", @@ -1521,7 +1518,6 @@ "dashboard.topNave.viewModeInteractiveSaveConfigDescription": "创建仪表板的副本", "dashboard.unsavedChangesBadge": "未保存的更改", "dashboard.unsavedChangesBadgeToolTipContent": "在此仪表板中有未保存更改。要移除此标签,请保存该仪表板。", - "dashboard.viewmodeBackup.error": "备份视图模式时出错:{message}", "data.advancedSettings.autocompleteIgnoreTimerange": "使用时间范围", "data.advancedSettings.autocompleteIgnoreTimerangeText": "禁用此属性可从您的完全数据集中获取自动完成建议,而非从当前时间范围。{learnMoreLink}", "data.advancedSettings.autocompleteValueSuggestionMethod": "自动填充值建议方法",