mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Dashboard] Store view mode in local storage (#166523)
Moves the Dashboard view mode from session storage to local storage. This means that users will only need to enter edit mode **once** if they are an editor, and any subsequent Dashboards they open will already be in edit mode.
This commit is contained in:
parent
7e32fc8432
commit
8ffa8d8ee4
26 changed files with 223 additions and 110 deletions
|
@ -70,9 +70,7 @@ describe('ShowShareModal', () => {
|
|||
const getPropsAndShare = (
|
||||
unsavedState?: Partial<DashboardContainerInput>
|
||||
): ShowShareModalProps => {
|
||||
pluginServices.getServices().dashboardSessionStorage.getState = jest
|
||||
.fn()
|
||||
.mockReturnValue(unsavedState);
|
||||
pluginServices.getServices().dashboardBackup.getState = jest.fn().mockReturnValue(unsavedState);
|
||||
return {
|
||||
isDirty: true,
|
||||
anchorElement: document.createElement('div'),
|
||||
|
|
|
@ -50,7 +50,7 @@ export function ShowShareModal({
|
|||
}: ShowShareModalProps) {
|
||||
const {
|
||||
dashboardCapabilities: { createShortUrl: allowShortUrl },
|
||||
dashboardSessionStorage,
|
||||
dashboardBackup,
|
||||
data: {
|
||||
query: {
|
||||
timefilter: {
|
||||
|
@ -121,7 +121,7 @@ export function ShowShareModal({
|
|||
};
|
||||
|
||||
let unsavedStateForLocator: DashboardAppLocatorParams = {};
|
||||
const unsavedDashboardState = dashboardSessionStorage.getState(savedObjectId);
|
||||
const unsavedDashboardState = dashboardBackup.getState(savedObjectId);
|
||||
|
||||
if (unsavedDashboardState) {
|
||||
unsavedStateForLocator = {
|
||||
|
|
|
@ -38,6 +38,7 @@ export const useDashboardMenuItems = ({
|
|||
*/
|
||||
const {
|
||||
share,
|
||||
dashboardBackup,
|
||||
settings: { uiSettings },
|
||||
dashboardCapabilities: { showWriteControls },
|
||||
} = pluginServices.getServices();
|
||||
|
@ -127,18 +128,24 @@ export const useDashboardMenuItems = ({
|
|||
const resetChanges = useCallback(
|
||||
(switchToViewMode: boolean = false) => {
|
||||
dashboard.clearOverlays();
|
||||
if (hasUnsavedChanges) {
|
||||
confirmDiscardUnsavedChanges(() => {
|
||||
batch(() => {
|
||||
dashboard.resetToLastSavedState();
|
||||
if (switchToViewMode) dashboard.dispatch.setViewMode(ViewMode.VIEW);
|
||||
});
|
||||
}, viewMode);
|
||||
} else {
|
||||
if (switchToViewMode) dashboard.dispatch.setViewMode(ViewMode.VIEW);
|
||||
const switchModes = switchToViewMode
|
||||
? () => {
|
||||
dashboard.dispatch.setViewMode(ViewMode.VIEW);
|
||||
dashboardBackup.storeViewMode(ViewMode.VIEW);
|
||||
}
|
||||
: undefined;
|
||||
if (!hasUnsavedChanges) {
|
||||
switchModes?.();
|
||||
return;
|
||||
}
|
||||
confirmDiscardUnsavedChanges(() => {
|
||||
batch(() => {
|
||||
dashboard.resetToLastSavedState();
|
||||
switchModes?.();
|
||||
});
|
||||
}, viewMode);
|
||||
},
|
||||
[dashboard, hasUnsavedChanges, viewMode]
|
||||
[dashboard, dashboardBackup, hasUnsavedChanges, viewMode]
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -170,6 +177,7 @@ export const useDashboardMenuItems = ({
|
|||
testId: 'dashboardEditMode',
|
||||
className: 'eui-hideFor--s eui-hideFor--xs', // hide for small screens - editing doesn't work in mobile mode.
|
||||
run: () => {
|
||||
dashboardBackup.storeViewMode(ViewMode.EDIT);
|
||||
dashboard.dispatch.setViewMode(ViewMode.EDIT);
|
||||
dashboard.clearOverlays();
|
||||
},
|
||||
|
@ -231,18 +239,19 @@ export const useDashboardMenuItems = ({
|
|||
} as TopNavMenuData,
|
||||
};
|
||||
}, [
|
||||
disableTopNav,
|
||||
quickSaveDashboard,
|
||||
isSaveInProgress,
|
||||
hasRunMigrations,
|
||||
hasUnsavedChanges,
|
||||
dashboardBackup,
|
||||
saveDashboardAs,
|
||||
setIsLabsShown,
|
||||
disableTopNav,
|
||||
resetChanges,
|
||||
isLabsShown,
|
||||
lastSavedId,
|
||||
showShare,
|
||||
dashboard,
|
||||
setIsLabsShown,
|
||||
isLabsShown,
|
||||
quickSaveDashboard,
|
||||
saveDashboardAs,
|
||||
resetChanges,
|
||||
clone,
|
||||
]);
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ export const DASHBOARD_CACHE_TTL = 1000 * 60 * 5; // time to live = 5 minutes
|
|||
// Default State
|
||||
// ------------------------------------------------------------------
|
||||
export const DEFAULT_DASHBOARD_INPUT: Omit<DashboardContainerInput, 'id'> = {
|
||||
viewMode: ViewMode.EDIT, // new dashboards start in edit mode.
|
||||
viewMode: ViewMode.VIEW,
|
||||
timeRestore: false,
|
||||
query: { query: '', language: 'kuery' },
|
||||
description: '',
|
||||
|
|
|
@ -83,7 +83,12 @@ export const dashboardSavedObjectErrorStrings = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const panelStorageErrorStrings = {
|
||||
export const backupServiceStrings = {
|
||||
viewModeStorageError: (message: string) =>
|
||||
i18n.translate('dashboard.viewmodeBackup.error', {
|
||||
defaultMessage: 'Error encountered while backing up view mode: {message}',
|
||||
values: { message },
|
||||
}),
|
||||
getPanelsGetError: (message: string) =>
|
||||
i18n.translate('dashboard.panelStorageError.getError', {
|
||||
defaultMessage: 'Error encountered while fetching unsaved changes: {message}',
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
ControlGroupContainerFactory,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { EmbeddablePackageState } from '@kbn/embeddable-plugin/public';
|
||||
import { EmbeddablePackageState, ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
||||
import { createDashboard } from './create_dashboard';
|
||||
|
@ -109,7 +109,55 @@ test('passes managed state from the saved object into the Dashboard component st
|
|||
expect(dashboard!.getState().componentState.managed).toBe(true);
|
||||
});
|
||||
|
||||
test('pulls state from session storage which overrides state from saved object', async () => {
|
||||
test('pulls view mode from dashboard backup', async () => {
|
||||
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
dashboardInput: DEFAULT_DASHBOARD_INPUT,
|
||||
});
|
||||
pluginServices.getServices().dashboardBackup.getViewMode = jest
|
||||
.fn()
|
||||
.mockReturnValue(ViewMode.EDIT);
|
||||
const dashboard = await createDashboard({ useSessionStorageIntegration: true }, 0, 'what-an-id');
|
||||
expect(dashboard).toBeDefined();
|
||||
expect(dashboard!.getState().explicitInput.viewMode).toBe(ViewMode.EDIT);
|
||||
});
|
||||
|
||||
test('new dashboards start in edit mode', async () => {
|
||||
pluginServices.getServices().dashboardBackup.getViewMode = jest
|
||||
.fn()
|
||||
.mockReturnValue(ViewMode.VIEW);
|
||||
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
newDashboardCreated: true,
|
||||
dashboardInput: {
|
||||
...DEFAULT_DASHBOARD_INPUT,
|
||||
description: 'wow this description is okay',
|
||||
},
|
||||
});
|
||||
const dashboard = await createDashboard({ useSessionStorageIntegration: true }, 0, 'wow-such-id');
|
||||
expect(dashboard).toBeDefined();
|
||||
expect(dashboard!.getState().explicitInput.viewMode).toBe(ViewMode.EDIT);
|
||||
});
|
||||
|
||||
test('managed dashboards start in view mode', async () => {
|
||||
pluginServices.getServices().dashboardBackup.getViewMode = jest
|
||||
.fn()
|
||||
.mockReturnValue(ViewMode.EDIT);
|
||||
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
dashboardInput: DEFAULT_DASHBOARD_INPUT,
|
||||
managed: true,
|
||||
});
|
||||
const dashboard = await createDashboard({}, 0, 'what-an-id');
|
||||
expect(dashboard).toBeDefined();
|
||||
expect(dashboard!.getState().componentState.managed).toBe(true);
|
||||
expect(dashboard!.getState().explicitInput.viewMode).toBe(ViewMode.VIEW);
|
||||
});
|
||||
|
||||
test('pulls state from backup which overrides state from saved object', async () => {
|
||||
pluginServices.getServices().dashboardContentManagement.loadDashboardState = jest
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
|
@ -118,7 +166,7 @@ test('pulls state from session storage which overrides state from saved object',
|
|||
description: 'wow this description is okay',
|
||||
},
|
||||
});
|
||||
pluginServices.getServices().dashboardSessionStorage.getState = jest
|
||||
pluginServices.getServices().dashboardBackup.getState = jest
|
||||
.fn()
|
||||
.mockReturnValue({ description: 'wow this description marginally better' });
|
||||
const dashboard = await createDashboard({ useSessionStorageIntegration: true }, 0, 'wow-such-id');
|
||||
|
@ -137,7 +185,7 @@ test('pulls state from creation options initial input which overrides all other
|
|||
description: 'wow this description is okay',
|
||||
},
|
||||
});
|
||||
pluginServices.getServices().dashboardSessionStorage.getState = jest
|
||||
pluginServices.getServices().dashboardBackup.getState = jest
|
||||
.fn()
|
||||
.mockReturnValue({ description: 'wow this description marginally better' });
|
||||
const dashboard = await createDashboard(
|
||||
|
|
|
@ -135,8 +135,9 @@ export const initializeDashboard = async ({
|
|||
controlGroup?: ControlGroupContainer;
|
||||
}) => {
|
||||
const {
|
||||
dashboardSessionStorage,
|
||||
dashboardBackup,
|
||||
embeddable: { getEmbeddableFactory },
|
||||
dashboardCapabilities: { showWriteControls },
|
||||
data: {
|
||||
query: queryService,
|
||||
search: { session },
|
||||
|
@ -170,24 +171,43 @@ export const initializeDashboard = async ({
|
|||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Gather input from session storage if integration is used.
|
||||
// Gather input from session storage and local storage if integration is used.
|
||||
// --------------------------------------------------------------------------------------
|
||||
const sessionStorageInput = ((): Partial<DashboardContainerInput> | undefined => {
|
||||
if (!useSessionStorageIntegration) return;
|
||||
return dashboardSessionStorage.getState(loadDashboardReturn.dashboardId);
|
||||
return dashboardBackup.getState(loadDashboardReturn.dashboardId);
|
||||
})();
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
// Combine input from saved object, session storage, & passed input to create initial input.
|
||||
// --------------------------------------------------------------------------------------
|
||||
const initialViewMode = (() => {
|
||||
if (loadDashboardReturn.managed || !showWriteControls) return ViewMode.VIEW;
|
||||
if (
|
||||
loadDashboardReturn.newDashboardCreated ||
|
||||
dashboardBackup.dashboardHasUnsavedEdits(loadDashboardReturn.dashboardId)
|
||||
) {
|
||||
return ViewMode.EDIT;
|
||||
}
|
||||
|
||||
return dashboardBackup.getViewMode();
|
||||
})();
|
||||
|
||||
const overrideInput = getInitialInput?.();
|
||||
const initialInput: DashboardContainerInput = cloneDeep({
|
||||
...DEFAULT_DASHBOARD_INPUT,
|
||||
...(loadDashboardReturn?.dashboardInput ?? {}),
|
||||
...sessionStorageInput,
|
||||
|
||||
...(initialViewMode ? { viewMode: initialViewMode } : {}),
|
||||
...overrideInput,
|
||||
});
|
||||
|
||||
// Back up any view mode passed in explicitly.
|
||||
if (overrideInput?.viewMode) {
|
||||
dashboardBackup.storeViewMode(overrideInput?.viewMode);
|
||||
}
|
||||
|
||||
initialInput.executionContext = {
|
||||
type: 'dashboard',
|
||||
description: initialInput.title,
|
||||
|
|
|
@ -402,13 +402,13 @@ export class DashboardContainer extends Container<InheritedChildInput, Dashboard
|
|||
|
||||
this.searchSessionId = searchSessionId;
|
||||
|
||||
this.updateInput(newInput);
|
||||
batch(() => {
|
||||
this.dispatch.setLastSavedInput(loadDashboardReturn?.dashboardInput);
|
||||
this.dispatch.setManaged(loadDashboardReturn?.managed);
|
||||
this.dispatch.setAnimatePanelTransforms(false); // prevents panels from animating on navigate.
|
||||
this.dispatch.setLastSavedId(newSavedObjectId);
|
||||
});
|
||||
this.updateInput(newInput);
|
||||
dashboardContainerReady$.next(this);
|
||||
};
|
||||
|
||||
|
|
|
@ -211,8 +211,8 @@ function backupUnsavedChanges(
|
|||
this: DashboardContainer,
|
||||
unsavedChanges: Partial<DashboardContainerInput>
|
||||
) {
|
||||
const { dashboardSessionStorage } = pluginServices.getServices();
|
||||
dashboardSessionStorage.setState(
|
||||
const { dashboardBackup } = pluginServices.getServices();
|
||||
dashboardBackup.setState(
|
||||
this.getDashboardSavedObjectId(),
|
||||
omit(unsavedChanges, keysToOmitFromSessionStorage)
|
||||
);
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
} from './_dashboard_listing_strings';
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { confirmDiscardUnsavedChanges } from './confirm_overlays';
|
||||
import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_session_storage/dashboard_session_storage_service';
|
||||
import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_backup/dashboard_backup_service';
|
||||
import { DashboardListingProps } from './types';
|
||||
|
||||
export interface DashboardListingEmptyPromptProps {
|
||||
|
@ -46,7 +46,7 @@ export const DashboardListingEmptyPrompt = ({
|
|||
}: DashboardListingEmptyPromptProps) => {
|
||||
const {
|
||||
application,
|
||||
dashboardSessionStorage,
|
||||
dashboardBackup,
|
||||
dashboardCapabilities: { showWriteControls },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
|
@ -77,8 +77,8 @@ export const DashboardListingEmptyPrompt = ({
|
|||
color="danger"
|
||||
onClick={() =>
|
||||
confirmDiscardUnsavedChanges(() => {
|
||||
dashboardSessionStorage.clearState(DASHBOARD_PANELS_UNSAVED_ID);
|
||||
setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges());
|
||||
dashboardBackup.clearState(DASHBOARD_PANELS_UNSAVED_ID);
|
||||
setUnsavedDashboardIds(dashboardBackup.getDashboardIdsWithUnsavedChanges());
|
||||
})
|
||||
}
|
||||
data-test-subj="discardDashboardPromptButton"
|
||||
|
@ -105,7 +105,7 @@ export const DashboardListingEmptyPrompt = ({
|
|||
isEditingFirstDashboard,
|
||||
createItem,
|
||||
disableCreateDashboardButton,
|
||||
dashboardSessionStorage,
|
||||
dashboardBackup,
|
||||
setUnsavedDashboardIds,
|
||||
goToDashboard,
|
||||
]);
|
||||
|
|
|
@ -14,7 +14,7 @@ import { findTestSubject } from '@elastic/eui/lib/test';
|
|||
|
||||
import { pluginServices } from '../services/plugin_services';
|
||||
import { DashboardUnsavedListing, DashboardUnsavedListingProps } from './dashboard_unsaved_listing';
|
||||
import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_session_storage/dashboard_session_storage_service';
|
||||
import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_backup/dashboard_backup_service';
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
|
||||
const makeDefaultProps = (): DashboardUnsavedListingProps => ({
|
||||
|
@ -91,7 +91,7 @@ describe('Unsaved listing', () => {
|
|||
waitFor(() => {
|
||||
component.update();
|
||||
expect(pluginServices.getServices().overlays.openConfirm).toHaveBeenCalled();
|
||||
expect(pluginServices.getServices().dashboardSessionStorage.clearState).toHaveBeenCalledWith(
|
||||
expect(pluginServices.getServices().dashboardBackup.clearState).toHaveBeenCalledWith(
|
||||
'dashboardUnsavedOne'
|
||||
);
|
||||
});
|
||||
|
@ -125,16 +125,16 @@ describe('Unsaved listing', () => {
|
|||
const { component } = mountWith({ props });
|
||||
waitFor(() => {
|
||||
component.update();
|
||||
expect(pluginServices.getServices().dashboardSessionStorage.clearState).toHaveBeenCalledWith(
|
||||
expect(pluginServices.getServices().dashboardBackup.clearState).toHaveBeenCalledWith(
|
||||
'failCase1'
|
||||
);
|
||||
expect(pluginServices.getServices().dashboardSessionStorage.clearState).toHaveBeenCalledWith(
|
||||
expect(pluginServices.getServices().dashboardBackup.clearState).toHaveBeenCalledWith(
|
||||
'failCase2'
|
||||
);
|
||||
|
||||
// clearing panels from dashboard with errors should cause getDashboardIdsWithUnsavedChanges to be called again.
|
||||
expect(
|
||||
pluginServices.getServices().dashboardSessionStorage.getDashboardIdsWithUnsavedChanges
|
||||
pluginServices.getServices().dashboardBackup.getDashboardIdsWithUnsavedChanges
|
||||
).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,7 +23,7 @@ import { pluginServices } from '../services/plugin_services';
|
|||
import { confirmDiscardUnsavedChanges } from './confirm_overlays';
|
||||
import { DashboardAttributes } from '../../common/content_management';
|
||||
import { dashboardUnsavedListingStrings, getNewDashboardTitle } from './_dashboard_listing_strings';
|
||||
import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_session_storage/dashboard_session_storage_service';
|
||||
import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_backup/dashboard_backup_service';
|
||||
|
||||
const DashboardUnsavedItem = ({
|
||||
id,
|
||||
|
@ -116,7 +116,7 @@ export const DashboardUnsavedListing = ({
|
|||
refreshUnsavedDashboards,
|
||||
}: DashboardUnsavedListingProps) => {
|
||||
const {
|
||||
dashboardSessionStorage,
|
||||
dashboardBackup,
|
||||
dashboardContentManagement: { findDashboards },
|
||||
} = pluginServices.getServices();
|
||||
|
||||
|
@ -132,11 +132,11 @@ export const DashboardUnsavedListing = ({
|
|||
const onDiscard = useCallback(
|
||||
(id?: string) => {
|
||||
confirmDiscardUnsavedChanges(() => {
|
||||
dashboardSessionStorage.clearState(id);
|
||||
dashboardBackup.clearState(id);
|
||||
refreshUnsavedDashboards();
|
||||
});
|
||||
},
|
||||
[refreshUnsavedDashboards, dashboardSessionStorage]
|
||||
[refreshUnsavedDashboards, dashboardBackup]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -156,7 +156,7 @@ export const DashboardUnsavedListing = ({
|
|||
const newItems = results.reduce((map, result) => {
|
||||
if (result.status === 'error') {
|
||||
hasError = true;
|
||||
dashboardSessionStorage.clearState(result.id);
|
||||
dashboardBackup.clearState(result.id);
|
||||
return map;
|
||||
}
|
||||
return {
|
||||
|
@ -173,7 +173,7 @@ export const DashboardUnsavedListing = ({
|
|||
return () => {
|
||||
canceled = true;
|
||||
};
|
||||
}, [refreshUnsavedDashboards, dashboardSessionStorage, unsavedDashboardIds, findDashboards]);
|
||||
}, [refreshUnsavedDashboards, dashboardBackup, unsavedDashboardIds, findDashboards]);
|
||||
|
||||
return unsavedDashboardIds.length === 0 ? null : (
|
||||
<>
|
||||
|
|
|
@ -46,15 +46,13 @@ describe('useDashboardListingTable', () => {
|
|||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
getPluginServices.dashboardSessionStorage.dashboardHasUnsavedEdits = jest
|
||||
.fn()
|
||||
.mockReturnValue(true);
|
||||
getPluginServices.dashboardBackup.dashboardHasUnsavedEdits = jest.fn().mockReturnValue(true);
|
||||
|
||||
getPluginServices.dashboardSessionStorage.getDashboardIdsWithUnsavedChanges = jest
|
||||
getPluginServices.dashboardBackup.getDashboardIdsWithUnsavedChanges = jest
|
||||
.fn()
|
||||
.mockReturnValue([]);
|
||||
|
||||
getPluginServices.dashboardSessionStorage.clearState = clearStateMock;
|
||||
getPluginServices.dashboardBackup.clearState = clearStateMock;
|
||||
getPluginServices.dashboardCapabilities.showWriteControls = true;
|
||||
getPluginServices.dashboardContentManagement.deleteDashboards = deleteDashboards;
|
||||
getPluginServices.settings.uiSettings.get = getUiSettingsMock;
|
||||
|
|
|
@ -87,7 +87,7 @@ export const useDashboardListingTable = ({
|
|||
showCreateDashboardButton?: boolean;
|
||||
}): UseDashboardListingTableReturnType => {
|
||||
const {
|
||||
dashboardSessionStorage,
|
||||
dashboardBackup,
|
||||
dashboardCapabilities: { showWriteControls },
|
||||
settings: { uiSettings },
|
||||
dashboardContentManagement: {
|
||||
|
@ -106,30 +106,30 @@ export const useDashboardListingTable = ({
|
|||
const [pageDataTestSubject, setPageDataTestSubject] = useState<string>();
|
||||
const [hasInitialFetchReturned, setHasInitialFetchReturned] = useState(false);
|
||||
const [unsavedDashboardIds, setUnsavedDashboardIds] = useState<string[]>(
|
||||
dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()
|
||||
dashboardBackup.getDashboardIdsWithUnsavedChanges()
|
||||
);
|
||||
|
||||
const listingLimit = uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING);
|
||||
const initialPageSize = uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING);
|
||||
|
||||
const createItem = useCallback(() => {
|
||||
if (useSessionStorageIntegration && dashboardSessionStorage.dashboardHasUnsavedEdits()) {
|
||||
if (useSessionStorageIntegration && dashboardBackup.dashboardHasUnsavedEdits()) {
|
||||
confirmCreateWithUnsaved(() => {
|
||||
dashboardSessionStorage.clearState();
|
||||
dashboardBackup.clearState();
|
||||
goToDashboard();
|
||||
}, goToDashboard);
|
||||
return;
|
||||
}
|
||||
goToDashboard();
|
||||
}, [dashboardSessionStorage, goToDashboard, useSessionStorageIntegration]);
|
||||
}, [dashboardBackup, goToDashboard, useSessionStorageIntegration]);
|
||||
|
||||
const updateItemMeta = useCallback(
|
||||
async (props: Pick<DashboardContainerInput, 'id' | 'title' | 'description' | 'tags'>) => {
|
||||
await updateDashboardMeta(props);
|
||||
|
||||
setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges());
|
||||
setUnsavedDashboardIds(dashboardBackup.getDashboardIdsWithUnsavedChanges());
|
||||
},
|
||||
[dashboardSessionStorage, updateDashboardMeta]
|
||||
[dashboardBackup, updateDashboardMeta]
|
||||
);
|
||||
|
||||
const contentEditorValidators: OpenContentEditorParams['customValidators'] = useMemo(
|
||||
|
@ -232,7 +232,7 @@ export const useDashboardListingTable = ({
|
|||
|
||||
await deleteDashboards(
|
||||
dashboardsToDelete.map(({ id }) => {
|
||||
dashboardSessionStorage.clearState(id);
|
||||
dashboardBackup.clearState(id);
|
||||
return id;
|
||||
})
|
||||
);
|
||||
|
@ -252,9 +252,9 @@ export const useDashboardListingTable = ({
|
|||
});
|
||||
}
|
||||
|
||||
setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges());
|
||||
setUnsavedDashboardIds(dashboardBackup.getDashboardIdsWithUnsavedChanges());
|
||||
},
|
||||
[dashboardSessionStorage, deleteDashboards, toasts]
|
||||
[dashboardBackup, deleteDashboards, toasts]
|
||||
);
|
||||
|
||||
const editItem = useCallback(
|
||||
|
@ -324,8 +324,8 @@ export const useDashboardListingTable = ({
|
|||
);
|
||||
|
||||
const refreshUnsavedDashboards = useCallback(
|
||||
() => setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()),
|
||||
[dashboardSessionStorage]
|
||||
() => setUnsavedDashboardIds(dashboardBackup.getDashboardIdsWithUnsavedChanges()),
|
||||
[dashboardBackup]
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
@ -7,16 +7,17 @@
|
|||
*/
|
||||
|
||||
import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public';
|
||||
import { DashboardSessionStorageServiceType } from './types';
|
||||
import { DashboardBackupServiceType } from './types';
|
||||
|
||||
type DashboardSessionStorageServiceFactory =
|
||||
PluginServiceFactory<DashboardSessionStorageServiceType>;
|
||||
type DashboardBackupServiceFactory = PluginServiceFactory<DashboardBackupServiceType>;
|
||||
|
||||
export const dashboardSessionStorageServiceFactory: DashboardSessionStorageServiceFactory = () => {
|
||||
export const dashboardBackupServiceFactory: DashboardBackupServiceFactory = () => {
|
||||
return {
|
||||
clearState: jest.fn(),
|
||||
getState: jest.fn().mockReturnValue(undefined),
|
||||
setState: jest.fn(),
|
||||
getViewMode: jest.fn(),
|
||||
storeViewMode: jest.fn(),
|
||||
getDashboardIdsWithUnsavedChanges: jest
|
||||
.fn()
|
||||
.mockReturnValue(['dashboardUnsavedOne', 'dashboardUnsavedTwo']),
|
|
@ -15,34 +15,37 @@ import type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/p
|
|||
|
||||
import { DashboardSpacesService } from '../spaces/types';
|
||||
import type { DashboardStartDependencies } from '../../plugin';
|
||||
import type { DashboardSessionStorageServiceType } from './types';
|
||||
import type { DashboardBackupServiceType } from './types';
|
||||
import type { DashboardContainerInput } from '../../../common';
|
||||
import { DashboardNotificationsService } from '../notifications/types';
|
||||
import { panelStorageErrorStrings } from '../../dashboard_container/_dashboard_container_strings';
|
||||
import { backupServiceStrings } from '../../dashboard_container/_dashboard_container_strings';
|
||||
|
||||
export const DASHBOARD_PANELS_UNSAVED_ID = 'unsavedDashboard';
|
||||
const DASHBOARD_PANELS_SESSION_KEY = 'dashboardStateManagerPanels';
|
||||
const DASHBOARD_VIEWMODE_LOCAL_KEY = 'dashboardViewMode';
|
||||
|
||||
interface DashboardSessionStorageRequiredServices {
|
||||
interface DashboardBackupRequiredServices {
|
||||
notifications: DashboardNotificationsService;
|
||||
spaces: DashboardSpacesService;
|
||||
}
|
||||
|
||||
export type DashboardSessionStorageServiceFactory = KibanaPluginServiceFactory<
|
||||
DashboardSessionStorageServiceType,
|
||||
export type DashboardBackupServiceFactory = KibanaPluginServiceFactory<
|
||||
DashboardBackupServiceType,
|
||||
DashboardStartDependencies,
|
||||
DashboardSessionStorageRequiredServices
|
||||
DashboardBackupRequiredServices
|
||||
>;
|
||||
|
||||
class DashboardSessionStorageService implements DashboardSessionStorageServiceType {
|
||||
class DashboardBackupService implements DashboardBackupServiceType {
|
||||
private activeSpaceId: string;
|
||||
private sessionStorage: Storage;
|
||||
private localStorage: Storage;
|
||||
private notifications: DashboardNotificationsService;
|
||||
private spaces: DashboardSpacesService;
|
||||
|
||||
constructor(requiredServices: DashboardSessionStorageRequiredServices) {
|
||||
constructor(requiredServices: DashboardBackupRequiredServices) {
|
||||
({ notifications: this.notifications, spaces: this.spaces } = requiredServices);
|
||||
this.sessionStorage = new Storage(sessionStorage);
|
||||
this.localStorage = new Storage(localStorage);
|
||||
|
||||
this.activeSpaceId = 'default';
|
||||
if (this.spaces.getActiveSpace$) {
|
||||
|
@ -52,6 +55,21 @@ class DashboardSessionStorageService implements DashboardSessionStorageServiceTy
|
|||
}
|
||||
}
|
||||
|
||||
public getViewMode = (): ViewMode => {
|
||||
return this.localStorage.get(DASHBOARD_VIEWMODE_LOCAL_KEY);
|
||||
};
|
||||
|
||||
public storeViewMode = (viewMode: ViewMode) => {
|
||||
try {
|
||||
this.localStorage.set(DASHBOARD_VIEWMODE_LOCAL_KEY, viewMode);
|
||||
} catch (e) {
|
||||
this.notifications.toasts.addDanger({
|
||||
title: backupServiceStrings.viewModeStorageError(e.message),
|
||||
'data-test-subj': 'dashboardViewmodeBackupFailure',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
public clearState(id = DASHBOARD_PANELS_UNSAVED_ID) {
|
||||
try {
|
||||
const sessionStorage = this.sessionStorage.get(DASHBOARD_PANELS_SESSION_KEY);
|
||||
|
@ -62,7 +80,7 @@ class DashboardSessionStorageService implements DashboardSessionStorageServiceTy
|
|||
}
|
||||
} catch (e) {
|
||||
this.notifications.toasts.addDanger({
|
||||
title: panelStorageErrorStrings.getPanelsClearError(e.message),
|
||||
title: backupServiceStrings.getPanelsClearError(e.message),
|
||||
'data-test-subj': 'dashboardPanelsClearFailure',
|
||||
});
|
||||
}
|
||||
|
@ -73,7 +91,7 @@ class DashboardSessionStorageService implements DashboardSessionStorageServiceTy
|
|||
return this.sessionStorage.get(DASHBOARD_PANELS_SESSION_KEY)?.[this.activeSpaceId]?.[id];
|
||||
} catch (e) {
|
||||
this.notifications.toasts.addDanger({
|
||||
title: panelStorageErrorStrings.getPanelsGetError(e.message),
|
||||
title: backupServiceStrings.getPanelsGetError(e.message),
|
||||
'data-test-subj': 'dashboardPanelsGetFailure',
|
||||
});
|
||||
}
|
||||
|
@ -86,7 +104,7 @@ class DashboardSessionStorageService implements DashboardSessionStorageServiceTy
|
|||
this.sessionStorage.set(DASHBOARD_PANELS_SESSION_KEY, sessionStateStorage);
|
||||
} catch (e) {
|
||||
this.notifications.toasts.addDanger({
|
||||
title: panelStorageErrorStrings.getPanelsSetError(e.message),
|
||||
title: backupServiceStrings.getPanelsSetError(e.message),
|
||||
'data-test-subj': 'dashboardPanelsSetFailure',
|
||||
});
|
||||
}
|
||||
|
@ -110,7 +128,7 @@ class DashboardSessionStorageService implements DashboardSessionStorageServiceTy
|
|||
return dashboardsWithUnsavedChanges;
|
||||
} catch (e) {
|
||||
this.notifications.toasts.addDanger({
|
||||
title: panelStorageErrorStrings.getPanelsGetError(e.message),
|
||||
title: backupServiceStrings.getPanelsGetError(e.message),
|
||||
'data-test-subj': 'dashboardPanelsGetFailure',
|
||||
});
|
||||
return [];
|
||||
|
@ -122,9 +140,9 @@ class DashboardSessionStorageService implements DashboardSessionStorageServiceTy
|
|||
}
|
||||
}
|
||||
|
||||
export const dashboardSessionStorageServiceFactory: DashboardSessionStorageServiceFactory = (
|
||||
export const dashboardBackupServiceFactory: DashboardBackupServiceFactory = (
|
||||
core,
|
||||
requiredServices
|
||||
) => {
|
||||
return new DashboardSessionStorageService(requiredServices);
|
||||
return new DashboardBackupService(requiredServices);
|
||||
};
|
|
@ -6,12 +6,15 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { ViewMode } from '@kbn/embeddable-plugin/public';
|
||||
import type { DashboardContainerInput } from '../../../common';
|
||||
|
||||
export interface DashboardSessionStorageServiceType {
|
||||
export interface DashboardBackupServiceType {
|
||||
clearState: (id?: string) => void;
|
||||
getState: (id: string | undefined) => Partial<DashboardContainerInput> | undefined;
|
||||
setState: (id: string | undefined, newState: Partial<DashboardContainerInput>) => void;
|
||||
getViewMode: () => ViewMode;
|
||||
storeViewMode: (viewMode: ViewMode) => void;
|
||||
getDashboardIdsWithUnsavedChanges: () => string[];
|
||||
dashboardHasUnsavedEdits: (id?: string) => boolean;
|
||||
}
|
|
@ -43,9 +43,9 @@ export const dashboardContentManagementServiceFactory: DashboardContentManagemen
|
|||
data,
|
||||
embeddable,
|
||||
notifications,
|
||||
dashboardBackup,
|
||||
initializerContext,
|
||||
savedObjectsTagging,
|
||||
dashboardSessionStorage,
|
||||
} = requiredServices;
|
||||
return {
|
||||
loadDashboardState: ({ id }) =>
|
||||
|
@ -64,10 +64,10 @@ export const dashboardContentManagementServiceFactory: DashboardContentManagemen
|
|||
lastSavedId,
|
||||
currentState,
|
||||
notifications,
|
||||
dashboardBackup,
|
||||
contentManagement,
|
||||
initializerContext,
|
||||
savedObjectsTagging,
|
||||
dashboardSessionStorage,
|
||||
}),
|
||||
findDashboards: {
|
||||
search: ({ hasReference, hasNoReference, search, size, options }) =>
|
||||
|
|
|
@ -56,7 +56,9 @@ export const loadDashboardState = async ({
|
|||
/**
|
||||
* This is a newly created dashboard, so there is no saved object state to load.
|
||||
*/
|
||||
if (!savedObjectId) return { dashboardInput: newDashboardState, dashboardFound: true };
|
||||
if (!savedObjectId) {
|
||||
return { dashboardInput: newDashboardState, dashboardFound: true, newDashboardCreated: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the saved object from Content Management
|
||||
|
|
|
@ -63,9 +63,9 @@ type SaveDashboardStateProps = SaveDashboardProps & {
|
|||
contentManagement: DashboardStartDependencies['contentManagement'];
|
||||
embeddable: DashboardContentManagementRequiredServices['embeddable'];
|
||||
notifications: DashboardContentManagementRequiredServices['notifications'];
|
||||
dashboardBackup: DashboardContentManagementRequiredServices['dashboardBackup'];
|
||||
initializerContext: DashboardContentManagementRequiredServices['initializerContext'];
|
||||
savedObjectsTagging: DashboardContentManagementRequiredServices['savedObjectsTagging'];
|
||||
dashboardSessionStorage: DashboardContentManagementRequiredServices['dashboardSessionStorage'];
|
||||
};
|
||||
|
||||
export const saveDashboardState = async ({
|
||||
|
@ -74,9 +74,9 @@ export const saveDashboardState = async ({
|
|||
lastSavedId,
|
||||
saveOptions,
|
||||
currentState,
|
||||
dashboardBackup,
|
||||
contentManagement,
|
||||
savedObjectsTagging,
|
||||
dashboardSessionStorage,
|
||||
notifications: { toasts },
|
||||
}: SaveDashboardStateProps): Promise<SaveDashboardReturn> => {
|
||||
const {
|
||||
|
@ -202,7 +202,7 @@ export const saveDashboardState = async ({
|
|||
* If the dashboard id has been changed, redirect to the new ID to keep the url param in sync.
|
||||
*/
|
||||
if (newId !== lastSavedId) {
|
||||
dashboardSessionStorage.clearState(lastSavedId);
|
||||
dashboardBackup.clearState(lastSavedId);
|
||||
return { redirectRequired: true, id: newId };
|
||||
} else {
|
||||
dashboardContentManagementCache.deleteDashboard(newId); // something changed in an existing dashboard, so delete it from the cache so that it can be re-fetched
|
||||
|
|
|
@ -23,7 +23,7 @@ import { DashboardCrudTypes } from '../../../common/content_management';
|
|||
import { DashboardScreenshotModeService } from '../screenshot_mode/types';
|
||||
import { DashboardInitializerContextService } from '../initializer_context/types';
|
||||
import { DashboardSavedObjectsTaggingService } from '../saved_objects_tagging/types';
|
||||
import { DashboardSessionStorageServiceType } from '../dashboard_session_storage/types';
|
||||
import { DashboardBackupServiceType } from '../dashboard_backup/types';
|
||||
import { DashboardDuplicateTitleCheckProps } from './lib/check_for_duplicate_dashboard_title';
|
||||
|
||||
export interface DashboardContentManagementRequiredServices {
|
||||
|
@ -31,10 +31,10 @@ export interface DashboardContentManagementRequiredServices {
|
|||
spaces: DashboardSpacesService;
|
||||
embeddable: DashboardEmbeddableService;
|
||||
notifications: DashboardNotificationsService;
|
||||
dashboardBackup: DashboardBackupServiceType;
|
||||
screenshotMode: DashboardScreenshotModeService;
|
||||
initializerContext: DashboardInitializerContextService;
|
||||
savedObjectsTagging: DashboardSavedObjectsTaggingService;
|
||||
dashboardSessionStorage: DashboardSessionStorageServiceType;
|
||||
}
|
||||
|
||||
export interface DashboardContentManagementService {
|
||||
|
@ -63,6 +63,7 @@ type DashboardResolveMeta = DashboardCrudTypes['GetOut']['meta'];
|
|||
|
||||
export interface LoadDashboardReturn {
|
||||
dashboardFound: boolean;
|
||||
newDashboardCreated?: boolean;
|
||||
dashboardId?: string;
|
||||
managed?: boolean;
|
||||
resolveMeta?: DashboardResolveMeta;
|
||||
|
|
|
@ -19,7 +19,7 @@ import { applicationServiceFactory } from './application/application.stub';
|
|||
import { chromeServiceFactory } from './chrome/chrome.stub';
|
||||
import { coreContextServiceFactory } from './core_context/core_context.stub';
|
||||
import { dashboardCapabilitiesServiceFactory } from './dashboard_capabilities/dashboard_capabilities.stub';
|
||||
import { dashboardSessionStorageServiceFactory } from './dashboard_session_storage/dashboard_session_storage.stub';
|
||||
import { dashboardBackupServiceFactory } from './dashboard_backup/dashboard_backup.stub';
|
||||
import { dataServiceFactory } from './data/data.stub';
|
||||
import { dataViewEditorServiceFactory } from './data_view_editor/data_view_editor.stub';
|
||||
import { documentationLinksServiceFactory } from './documentation_links/documentation_links.stub';
|
||||
|
@ -51,7 +51,7 @@ export const providers: PluginServiceProviders<DashboardServices> = {
|
|||
chrome: new PluginServiceProvider(chromeServiceFactory),
|
||||
coreContext: new PluginServiceProvider(coreContextServiceFactory),
|
||||
dashboardCapabilities: new PluginServiceProvider(dashboardCapabilitiesServiceFactory),
|
||||
dashboardSessionStorage: new PluginServiceProvider(dashboardSessionStorageServiceFactory),
|
||||
dashboardBackup: new PluginServiceProvider(dashboardBackupServiceFactory),
|
||||
data: new PluginServiceProvider(dataServiceFactory),
|
||||
dataViewEditor: new PluginServiceProvider(dataViewEditorServiceFactory),
|
||||
documentationLinks: new PluginServiceProvider(documentationLinksServiceFactory),
|
||||
|
|
|
@ -19,7 +19,7 @@ import { applicationServiceFactory } from './application/application_service';
|
|||
import { chromeServiceFactory } from './chrome/chrome_service';
|
||||
import { coreContextServiceFactory } from './core_context/core_context_service';
|
||||
import { dashboardCapabilitiesServiceFactory } from './dashboard_capabilities/dashboard_capabilities_service';
|
||||
import { dashboardSessionStorageServiceFactory } from './dashboard_session_storage/dashboard_session_storage_service';
|
||||
import { dashboardBackupServiceFactory } from './dashboard_backup/dashboard_backup_service';
|
||||
import { dataServiceFactory } from './data/data_service';
|
||||
import { dataViewEditorServiceFactory } from './data_view_editor/data_view_editor_service';
|
||||
import { documentationLinksServiceFactory } from './documentation_links/documentation_links_service';
|
||||
|
@ -47,16 +47,16 @@ import { noDataPageServiceFactory } from './no_data_page/no_data_page_service';
|
|||
|
||||
const providers: PluginServiceProviders<DashboardServices, DashboardPluginServiceParams> = {
|
||||
dashboardContentManagement: new PluginServiceProvider(dashboardContentManagementServiceFactory, [
|
||||
'dashboardSessionStorage',
|
||||
'savedObjectsTagging',
|
||||
'initializerContext',
|
||||
'dashboardBackup',
|
||||
'screenshotMode',
|
||||
'notifications',
|
||||
'embeddable',
|
||||
'spaces',
|
||||
'data',
|
||||
]),
|
||||
dashboardSessionStorage: new PluginServiceProvider(dashboardSessionStorageServiceFactory, [
|
||||
dashboardBackup: new PluginServiceProvider(dashboardBackupServiceFactory, [
|
||||
'notifications',
|
||||
'spaces',
|
||||
]),
|
||||
|
|
|
@ -19,7 +19,7 @@ import { DashboardCoreContextService } from './core_context/types';
|
|||
import { DashboardCustomBrandingService } from './custom_branding/types';
|
||||
import { DashboardCapabilitiesService } from './dashboard_capabilities/types';
|
||||
import { DashboardContentManagementService } from './dashboard_content_management/types';
|
||||
import { DashboardSessionStorageServiceType } from './dashboard_session_storage/types';
|
||||
import { DashboardBackupServiceType } from './dashboard_backup/types';
|
||||
import { DashboardDataService } from './data/types';
|
||||
import { DashboardDataViewEditorService } from './data_view_editor/types';
|
||||
import { DashboardDocumentationLinksService } from './documentation_links/types';
|
||||
|
@ -44,7 +44,7 @@ export type DashboardPluginServiceParams = KibanaPluginServiceParams<DashboardSt
|
|||
initContext: PluginInitializerContext; // need a custom type so that initContext is a required parameter for initializerContext
|
||||
};
|
||||
export interface DashboardServices {
|
||||
dashboardSessionStorage: DashboardSessionStorageServiceType;
|
||||
dashboardBackup: DashboardBackupServiceType;
|
||||
dashboardContentManagement: DashboardContentManagementService;
|
||||
|
||||
analytics: DashboardAnalyticsService;
|
||||
|
|
|
@ -38,6 +38,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('existing dashboard opens in last used view mode', async function () {
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
await PageObjects.dashboard.loadSavedDashboard(dashboardName);
|
||||
expect(await PageObjects.dashboard.getIsInViewMode()).to.equal(true);
|
||||
|
||||
await PageObjects.dashboard.switchToEditMode();
|
||||
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
await PageObjects.dashboard.loadSavedDashboard(dashboardName);
|
||||
expect(await PageObjects.dashboard.getIsInViewMode()).to.equal(false);
|
||||
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
await PageObjects.dashboard.loadSavedDashboard('few panels');
|
||||
expect(await PageObjects.dashboard.getIsInViewMode()).to.equal(false);
|
||||
|
||||
await PageObjects.dashboard.clickCancelOutOfEditMode();
|
||||
});
|
||||
|
||||
it('create new dashboard opens in edit mode', async function () {
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
await PageObjects.dashboard.clickNewDashboard();
|
||||
|
@ -45,14 +63,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
expect(isInViewMode).to.be(false);
|
||||
});
|
||||
|
||||
it('existing dashboard opens in view mode', async function () {
|
||||
await PageObjects.dashboard.gotoDashboardLandingPage();
|
||||
await PageObjects.dashboard.loadSavedDashboard(dashboardName);
|
||||
const inViewMode = await PageObjects.dashboard.getIsInViewMode();
|
||||
|
||||
expect(inViewMode).to.equal(true);
|
||||
});
|
||||
|
||||
describe('save', function () {
|
||||
it('auto exits out of edit mode', async function () {
|
||||
await PageObjects.dashboard.gotoDashboardEditMode(dashboardName);
|
||||
|
|
|
@ -182,11 +182,11 @@ export class DashboardPageObject extends FtrService {
|
|||
|
||||
public async expectOnDashboard(expectedTitle: string) {
|
||||
await this.retry.waitFor(
|
||||
`last breadcrumb to have dashboard title: ${expectedTitle}`,
|
||||
`last breadcrumb to have dashboard title: ${expectedTitle} OR Editing ${expectedTitle}`,
|
||||
async () => {
|
||||
const actualTitle = await this.globalNav.getLastBreadcrumb();
|
||||
this.log.debug(`Expected dashboard title ${expectedTitle}, actual: ${actualTitle}`);
|
||||
return actualTitle === expectedTitle;
|
||||
return actualTitle === expectedTitle || actualTitle === `Editing ${expectedTitle}`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue