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 17fa48e7473c2..c1cec1b85d00b 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 @@ -9,7 +9,6 @@ import { ContentInsightsClient } from '@kbn/content-management-content-insights-public'; 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 { getDashboardApi } from '../get_dashboard_api'; @@ -20,6 +19,10 @@ import { getUserAccessControlData } from './get_user_access_control_data'; import { dashboardClient } from '../../dashboard_client'; import { getLastSavedState } from '../default_dashboard_state'; import { DASHBOARD_DURATION_START_MARK } from '../performance/dashboard_duration_start_mark'; +import { + getDashboardBackupService, + initializeDashboardApiServices, +} from '../../services/dashboard_api_services'; export async function loadDashboardApi({ getCreationOptions, @@ -46,6 +49,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 1bbc294cff222..a24a1c4240c9d 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 4d784beaabf00..976f086904d07 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 @@ -23,8 +23,6 @@ import type { initializeProjectRoutingManager } from './project_routing_manager' import type { DashboardPanel } from '../../server'; import { getSampleDashboardState } from '../mocks'; -jest.mock('../services/dashboard_backup_service', () => ({})); - const controlGroupApi = { hasUnsavedChanges$: new BehaviorSubject(false), }; @@ -39,6 +37,7 @@ const controlGroupManagerMock = { }), }, } as unknown as ReturnType; + const layoutUnsavedChanges$ = new BehaviorSubject<{ panels?: DashboardState['panels'] }>({}); const layoutManagerMock = { api: { @@ -95,7 +94,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 839798e85cf22..549bd0b3386af 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 @@ -27,7 +27,7 @@ import { tap, } from 'rxjs'; import type { DashboardBackupState } from '../services/dashboard_backup_service'; -import { getDashboardBackupService } from '../services/dashboard_backup_service'; +import { getDashboardBackupService } from '../services/dashboard_api_services'; import type { initializeLayoutManager } from './layout_manager'; import type { initializeSettingsManager } from './settings_manager'; import type { DashboardState } from '../../common'; 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 a0d04805b9579..f0075afcaa629 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 9d7b4f818f5d5..546ab66ce025a 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/show_share_modal.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.test.tsx index e8c16a8169cde..5862e1f54b0b1 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 } 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/share/show_share_modal.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx index 2115958f50844..f33635c3718ca 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/share/show_share_modal.tsx @@ -27,7 +27,7 @@ import { import { DASHBOARD_SAVED_OBJECT_TYPE } from '@kbn/deeplinks-analytics/constants'; import type { DashboardLocatorParams } from '../../../../common'; -import { getDashboardBackupService } from '../../../services/dashboard_backup_service'; +import { getDashboardBackupService } from '../../../services/dashboard_api_services'; import { dataService, shareService, 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 ae2e57d583f4e..e42f67e906669 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 @@ -20,7 +20,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 4efb385c19d8c..56be14e300e61 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]) => { @@ -128,9 +126,9 @@ export const useDashboardListingTable = ({ dashboard.references ); - setUnsavedDashboardIds(dashboardBackupService.getDashboardIdsWithUnsavedChanges()); + setUnsavedDashboardIds(getDashboardBackupService().getDashboardIdsWithUnsavedChanges()); }, - [dashboardBackupService] + [] ); const contentEditorValidators: OpenContentEditorParams['customValidators'] = useMemo( @@ -270,35 +268,32 @@ export const useDashboardListingTable = ({ [listingLimit, accessControlClient] ); - 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); - }); - - 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 deleteItems = useCallback(async (dashboardsToDelete: Array<{ id: string }>) => { + try { + const deleteStartTime = window.performance.now(); + + 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(), + }); + } - 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 60b6a88f46def..82f3b5c2325be 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 9724ba92217e7..2afb0d27b1a3b 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 @@ -18,3 +18,4 @@ export { AddToLibraryAction } from '../dashboard_actions/library_add_action'; export { UnlinkFromLibraryAction } from '../dashboard_actions/library_unlink_action'; export { CopyToDashboardAction } from '../dashboard_actions/copy_to_dashboard_action'; export { AddSectionAction } from '../dashboard_actions/add_section_action'; +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 cfca392c91245..9fda454799a67 100644 --- a/src/platform/plugins/shared/dashboard/public/plugin.tsx +++ b/src/platform/plugins/shared/dashboard/public/plugin.tsx @@ -70,7 +70,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'; @@ -225,14 +225,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 9a8a5be26ac91..790185e0eb5b8 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'; @@ -28,8 +28,6 @@ import type { SpacesApi } from '@kbn/spaces-plugin/public'; import type { UiActionsPublicStart } from '@kbn/ui-actions-plugin/public/plugin'; 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 { DashboardStartDependencies } from '../plugin'; export let coreServices: CoreStart; @@ -55,6 +53,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 decd97a984dbb..0e485dd6d4700 100644 --- a/x-pack/platform/plugins/private/translations/translations/de-DE.json +++ b/x-pack/platform/plugins/private/translations/translations/de-DE.json @@ -1530,9 +1530,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", @@ -1580,7 +1577,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 87ba6307538bd..3d527051ad5c5 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -1549,9 +1549,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", @@ -1599,7 +1596,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 071480c32b9e3..52887103b391a 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -1550,9 +1550,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": "ダッシュボードが見つかりません", @@ -1600,7 +1597,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 c735768224e08..4a6b0aee80929 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -1544,9 +1544,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": "找不到仪表板", @@ -1594,7 +1591,6 @@ "dashboard.topNave.viewModeInteractiveSaveConfigDescription": "创建仪表板的副本", "dashboard.unsavedChangesBadge": "未保存的更改", "dashboard.unsavedChangesBadgeToolTipContent": "在此仪表板中有未保存更改。要移除此标签,请保存该仪表板。", - "dashboard.viewmodeBackup.error": "备份视图模式时出错:{message}", "data.advancedSettings.autocompleteIgnoreTimerange": "使用时间范围", "data.advancedSettings.autocompleteIgnoreTimerangeText": "禁用此属性可从您的完全数据集中获取自动完成建议,而非从当前时间范围。{learnMoreLink}", "data.advancedSettings.autocompleteValueSuggestionMethod": "自动填充值建议方法",