Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Time to Visualize] Remove Panels from URL #86939

Merged
merged 43 commits into from
Feb 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1e2bb42
First pass at removing panels from URL. Everything should work
ThomThomson Dec 24, 2020
8b2ac8b
Fix types
ThomThomson Dec 24, 2020
aa1140e
First version of unsaved panels listing
ThomThomson Jan 8, 2021
ac24142
Merge branch 'master' of github.com:elastic/kibana into feature/remov…
ThomThomson Jan 11, 2021
946c7c4
type fix
ThomThomson Jan 12, 2021
d4b0c11
Added confirm to create new when new dashboard already has unsaved ed…
ThomThomson Jan 12, 2021
25da78c
Fixed typo, ensured that snapshot share links work properly
ThomThomson Jan 13, 2021
ea9162d
Added unit testing for unsaved listing
ThomThomson Jan 14, 2021
de19cb7
Changed recently accessed functionality. Ensured proper unsaved state…
ThomThomson Jan 14, 2021
1699180
Merge branch 'master' of github.com:elastic/kibana into feature/remov…
ThomThomson Jan 14, 2021
e5f68d9
Removed issue caused by dashboard_security importing from dashboard_c…
ThomThomson Jan 14, 2021
c18bc1f
Merge branch 'master' of github.com:elastic/kibana into feature/remov…
ThomThomson Jan 18, 2021
b9e1c9a
split discarding changes from view / edit mode.
ThomThomson Jan 18, 2021
03214ef
Listing entries for Dashboards with unsaved edits now open in view mo…
ThomThomson Jan 18, 2021
c19d073
doc title change for New Dashboards, removed redirectTo from onDiscar…
ThomThomson Jan 19, 2021
0e6f6b2
fixing functional tests
ThomThomson Jan 19, 2021
f2cc036
added aria-labels to edit and discard buttons in unsaved listing
ThomThomson Jan 19, 2021
9503272
i18n fix
ThomThomson Jan 19, 2021
809199a
design tweaks to align with notifications UI
ryankeairns Jan 19, 2021
976b40d
Merge pull request #3 from ryankeairns/feature/removePanelsFromUrl
ThomThomson Jan 19, 2021
e7304a0
Merge branch 'master' into feature/removePanelsFromUrl
kibanamachine Jan 20, 2021
0a46cbb
Updated license headers, made both listing page create buttons use th…
ThomThomson Jan 20, 2021
0465fe3
worked around functional tests clearing session storage after each te…
ThomThomson Jan 21, 2021
dafb75b
reimplemented functional tests which manually edit the URL by using t…
ThomThomson Jan 21, 2021
be357af
manually create new dashboard in panel_context_menu test
ThomThomson Jan 21, 2021
634a8cc
revert cancel button name, remove panels retained toast
ThomThomson Jan 21, 2021
930087c
functional test fixes
ThomThomson Jan 25, 2021
1929a75
Merge branch 'master' of github.com:elastic/kibana into feature/remov…
ThomThomson Jan 25, 2021
3231d79
restored default color
ThomThomson Jan 26, 2021
2aab904
Added functional tests
ThomThomson Jan 27, 2021
53b2065
fixed jest
ThomThomson Jan 27, 2021
3f63de0
Merge branch 'master' of github.com:elastic/kibana into feature/remov…
ThomThomson Jan 27, 2021
9bdbaa2
Merge branch 'master' into feature/removePanelsFromUrl
kibanamachine Jan 27, 2021
09de507
Add responsive styles
ryankeairns Jan 28, 2021
55cda0e
Merge pull request #4 from ryankeairns/feature/removePanelsFromUrl
ThomThomson Jan 28, 2021
3d1cfeb
error handling for dashboard panel storage.
ThomThomson Jan 28, 2021
0190c26
Merge branch 'master' of github.com:elastic/kibana into feature/remov…
ThomThomson Jan 28, 2021
efd5b3f
refactor dashboard unsaved listing to show entries when loading
ThomThomson Jan 29, 2021
c714336
Merge branch 'master' into feature/removePanelsFromUrl
kibanamachine Feb 1, 2021
0bf8e68
revert import in dashboard_constants
ThomThomson Feb 1, 2021
d433d04
Merge branch 'feature/removePanelsFromUrl' of github.com:thomthomson/…
ThomThomson Feb 1, 2021
b4355d9
changed unsaved listing cancelation logic
ThomThomson Feb 3, 2021
c355032
Merge branch 'master' of github.com:elastic/kibana into feature/remov…
ThomThomson Feb 3, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/plugins/dashboard/public/application/_dashboard_app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,37 @@
margin-left: $euiSizeS;
text-align: center;
}

.dshUnsavedListingItem {
margin-top: $euiSizeM;
}

.dshUnsavedListingItem__icon {
margin-right: $euiSizeM;
}

.dshUnsavedListingItem__title {
margin-bottom: 0 !important;
}

.dshUnsavedListingItem__loading {
color: $euiTextSubduedColor !important;
}

.dshUnsavedListingItem__actions {
margin-left: $euiSizeL + $euiSizeXS;
}

@include euiBreakpoint('xs', 's') {
.dshUnsavedListingItem {
margin-top: $euiSize;
}

.dshUnsavedListingItem__heading {
margin-bottom: $euiSizeXS;
}

.dshUnsavedListingItem__actions {
flex-direction: column;
}
}
14 changes: 11 additions & 3 deletions src/plugins/dashboard/public/application/dashboard_router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ import { Switch, Route, RouteComponentProps, HashRouter, Redirect } from 'react-

import { DashboardListing } from './listing';
import { DashboardApp } from './dashboard_app';
import { addHelpMenuToAppChrome } from './lib';
import { addHelpMenuToAppChrome, DashboardPanelStorage } from './lib';
import { createDashboardListingFilterUrl } from '../dashboard_constants';
import { getDashboardPageTitle, dashboardReadonlyBadge } from '../dashboard_strings';
import { createDashboardEditUrl, DashboardConstants } from '../dashboard_constants';
import { DashboardAppServices, DashboardEmbedSettings, RedirectToProps } from './types';
import { DashboardSetupDependencies, DashboardStart, DashboardStartDependencies } from '../plugin';
import {
DashboardFeatureFlagConfig,
DashboardSetupDependencies,
DashboardStart,
DashboardStartDependencies,
} from '../plugin';

import { createKbnUrlStateStorage, withNotifyOnErrors } from '../services/kibana_utils';
import { KibanaContextProvider } from '../services/kibana_react';
Expand Down Expand Up @@ -94,8 +99,11 @@ export async function mountApp({
indexPatterns: dataStart.indexPatterns,
savedQueryService: dataStart.query.savedQueries,
savedObjectsClient: coreStart.savedObjects.client,
dashboardPanelStorage: new DashboardPanelStorage(core.notifications.toasts),
savedDashboards: dashboardStart.getSavedDashboardLoader(),
savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(),
allowByValueEmbeddables: initializerContext.config.get<DashboardFeatureFlagConfig>()
.allowByValueEmbeddables,
dashboardCapabilities: {
hideWriteControls: dashboardConfig.getHideWriteControls(),
show: Boolean(coreStart.application.capabilities.dashboard.show),
Expand All @@ -122,7 +130,7 @@ export async function mountApp({
let destination;
if (redirectTo.destination === 'dashboard') {
destination = redirectTo.id
? createDashboardEditUrl(redirectTo.id)
? createDashboardEditUrl(redirectTo.id, redirectTo.editMode)
: DashboardConstants.CREATE_NEW_DASHBOARD_URL;
} else {
destination = createDashboardListingFilterUrl(redirectTo.filter);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ describe('DashboardState', function () {
dashboardState = new DashboardStateManager({
savedDashboard,
hideWriteControls: false,
allowByValueEmbeddables: false,
kibanaVersion: '7.0.0',
kbnUrlStateStorage: createKbnUrlStateStorage(),
history: createBrowserHistory(),
Expand Down
164 changes: 116 additions & 48 deletions src/plugins/dashboard/public/application/dashboard_state_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ import { FilterUtils } from './lib/filter_utils';
import { DashboardContainer } from './embeddable';
import { DashboardSavedObject } from '../saved_dashboards';
import { migrateLegacyQuery } from './lib/migrate_legacy_query';
import { getAppStateDefaults, migrateAppState, getDashboardIdFromUrl } from './lib';
import {
getAppStateDefaults,
migrateAppState,
getDashboardIdFromUrl,
DashboardPanelStorage,
} from './lib';
import { convertPanelStateToSavedDashboardPanel } from '../../common/embeddable/embeddable_saved_object_converters';
import {
DashboardAppState,
Expand All @@ -37,6 +42,7 @@ import {
ReduxLikeStateContainer,
syncState,
} from '../services/kibana_utils';
import { STATE_STORAGE_KEY } from '../url_generator';

/**
* Dashboard state manager handles connecting angular and redux state between the angular and react portions of the
Expand Down Expand Up @@ -71,10 +77,11 @@ export class DashboardStateManager {
DashboardAppStateTransitions
>;
private readonly stateContainerChangeSub: Subscription;
private readonly STATE_STORAGE_KEY = '_a';
private readonly dashboardPanelStorage?: DashboardPanelStorage;
public readonly kbnUrlStateStorage: IKbnUrlStateStorage;
private readonly stateSyncRef: ISyncStateRef;
private readonly history: History;
private readonly allowByValueEmbeddables: boolean;

private readonly usageCollection: UsageCollectionSetup | undefined;
public readonly hasTaggingCapabilities: SavedObjectTagDecoratorTypeGuard;

Expand All @@ -86,49 +93,62 @@ export class DashboardStateManager {
* @param
*/
constructor({
history,
kibanaVersion,
savedDashboard,
usageCollection,
hideWriteControls,
kibanaVersion,
kbnUrlStateStorage,
history,
usageCollection,
dashboardPanelStorage,
hasTaggingCapabilities,
allowByValueEmbeddables,
}: {
savedDashboard: DashboardSavedObject;
hideWriteControls: boolean;
kibanaVersion: string;
kbnUrlStateStorage: IKbnUrlStateStorage;
history: History;
kibanaVersion: string;
hideWriteControls: boolean;
allowByValueEmbeddables: boolean;
savedDashboard: DashboardSavedObject;
usageCollection?: UsageCollectionSetup;
kbnUrlStateStorage: IKbnUrlStateStorage;
dashboardPanelStorage?: DashboardPanelStorage;
hasTaggingCapabilities: SavedObjectTagDecoratorTypeGuard;
}) {
this.history = history;
this.kibanaVersion = kibanaVersion;
this.savedDashboard = savedDashboard;
this.hideWriteControls = hideWriteControls;
this.usageCollection = usageCollection;
this.hasTaggingCapabilities = hasTaggingCapabilities;
this.allowByValueEmbeddables = allowByValueEmbeddables;

// get state defaults from saved dashboard, make sure it is migrated
this.stateDefaults = migrateAppState(
getAppStateDefaults(this.savedDashboard, this.hideWriteControls, this.hasTaggingCapabilities),
kibanaVersion,
usageCollection
);

this.dashboardPanelStorage = dashboardPanelStorage;
this.kbnUrlStateStorage = kbnUrlStateStorage;

// setup initial state by merging defaults with state from url
// setup initial state by merging defaults with state from url & panels storage
// also run migration, as state in url could be of older version
const initialUrlState = this.kbnUrlStateStorage.get<DashboardAppState>(STATE_STORAGE_KEY);
const initialState = migrateAppState(
{
...this.stateDefaults,
...this.kbnUrlStateStorage.get<DashboardAppState>(this.STATE_STORAGE_KEY),
...this.getUnsavedPanelState(),
...initialUrlState,
},
kibanaVersion,
usageCollection
);

this.isDirty = false;

if (initialUrlState?.panels && !_.isEqual(initialUrlState.panels, this.stateDefaults.panels)) {
this.isDirty = true;
this.setUnsavedPanels(initialState.panels);
}

// setup state container using initial state both from defaults and from url
this.stateContainer = createStateContainer<DashboardAppState, DashboardAppStateTransitions>(
initialState,
Expand All @@ -144,8 +164,6 @@ export class DashboardStateManager {
}
);

this.isDirty = false;

// We can't compare the filters stored on this.appState to this.savedDashboard because in order to apply
// the filters to the visualizations, we need to save it on the dashboard. We keep track of the original
// filter state in order to let the user know if their filters changed and provide this specific information
Expand All @@ -159,16 +177,16 @@ export class DashboardStateManager {
this.changeListeners.forEach((listener) => listener({ dirty: this.isDirty }));
});

// setup state syncing utils. state container will be synced with url into `this.STATE_STORAGE_KEY` query param
// setup state syncing utils. state container will be synced with url into `STATE_STORAGE_KEY` query param
this.stateSyncRef = syncState<DashboardAppStateInUrl>({
storageKey: this.STATE_STORAGE_KEY,
storageKey: STATE_STORAGE_KEY,
stateContainer: {
...this.stateContainer,
get: () => this.toUrlState(this.stateContainer.get()),
set: (state: DashboardAppStateInUrl | null) => {
set: (stateFromUrl: DashboardAppStateInUrl | null) => {
// sync state required state container to be able to handle null
// overriding set() so it could handle null coming from url
if (state) {
if (stateFromUrl) {
// Skip this update if current dashboardId in the url is different from what we have in the current instance of state manager
// As dashboard is driven by angular at the moment, the destroy cycle happens async,
// If the dashboardId has changed it means this instance
Expand All @@ -177,9 +195,15 @@ export class DashboardStateManager {
const currentDashboardIdInUrl = getDashboardIdFromUrl(history.location.pathname);
if (currentDashboardIdInUrl !== this.savedDashboard.id) return;

// set View mode before the rest of the state so unsaved panels can be added correctly.
if (this.appState.viewMode !== stateFromUrl.viewMode) {
this.switchViewMode(stateFromUrl.viewMode);
}

this.stateContainer.set({
...this.stateDefaults,
...state,
...this.getUnsavedPanelState(),
...stateFromUrl,
});
} else {
// Do nothing in case when state from url is empty,
Expand Down Expand Up @@ -261,6 +285,13 @@ export class DashboardStateManager {
if (dirtyBecauseOfInitialStateMigration) {
this.saveState({ replace: true });
}

// If a panel has been changed, and the state is now equal to the state in the saved object, remove the unsaved panels
if (!this.isDirty && this.getIsEditMode()) {
this.clearUnsavedPanels();
} else {
this.setUnsavedPanels(this.getPanels());
}
}

if (input.isFullScreenMode !== this.getFullScreenMode()) {
Expand Down Expand Up @@ -483,7 +514,16 @@ export class DashboardStateManager {
}

public getViewMode() {
return this.hideWriteControls ? ViewMode.VIEW : this.appState.viewMode;
if (this.hideWriteControls) {
return ViewMode.VIEW;
}
if (this.stateContainer) {
return this.appState.viewMode;
}
// get viewMode should work properly even before the state container is created
return this.savedDashboard.id
? this.kbnUrlStateStorage.get<DashboardAppState>(STATE_STORAGE_KEY)?.viewMode ?? ViewMode.VIEW
: ViewMode.EDIT;
}

public getIsViewMode() {
Expand Down Expand Up @@ -592,29 +632,13 @@ export class DashboardStateManager {
private saveState({ replace }: { replace: boolean }): boolean {
// schedules setting current state to url
this.kbnUrlStateStorage.set<DashboardAppStateInUrl>(
this.STATE_STORAGE_KEY,
STATE_STORAGE_KEY,
this.toUrlState(this.stateContainer.get())
);
// immediately forces scheduled updates and changes location
return !!this.kbnUrlStateStorage.kbnUrlControls.flush(replace);
}

// TODO: find nicer solution for this
// this function helps to make just 1 browser history update, when we imperatively changing the dashboard url
// It could be that there is pending *dashboardStateManager* updates, which aren't flushed yet to the url.
// So to prevent 2 browser updates:
// 1. Force flush any pending state updates (syncing state to query)
// 2. If url was updated, then apply path change with replace
public changeDashboardUrl(pathname: string) {
// synchronously persist current state to url with push()
const updated = this.saveState({ replace: false });
// change pathname
this.history[updated ? 'replace' : 'push']({
...this.history.location,
pathname,
});
}

public setQuery(query: Query) {
this.stateContainer.transitions.set('query', query);
}
Expand Down Expand Up @@ -644,6 +668,59 @@ export class DashboardStateManager {
}
}

public restorePanels() {
const unsavedState = this.getUnsavedPanelState();
if (!unsavedState || unsavedState.panels?.length === 0) {
return;
}
this.stateContainer.set(
migrateAppState(
{
...this.stateDefaults,
...unsavedState,
...this.kbnUrlStateStorage.get<DashboardAppState>(STATE_STORAGE_KEY),
},
this.kibanaVersion,
this.usageCollection
)
);
}

public clearUnsavedPanels() {
if (!this.allowByValueEmbeddables || !this.dashboardPanelStorage) {
return;
}
this.dashboardPanelStorage.clearPanels(this.savedDashboard?.id);
}

private getUnsavedPanelState(): { panels?: SavedDashboardPanel[] } {
if (!this.allowByValueEmbeddables || this.getIsViewMode() || !this.dashboardPanelStorage) {
return {};
}
const panels = this.dashboardPanelStorage.getPanels(this.savedDashboard?.id);
return panels ? { panels } : {};
}

private setUnsavedPanels(newPanels: SavedDashboardPanel[]) {
if (
!this.allowByValueEmbeddables ||
this.getIsViewMode() ||
!this.getIsDirty() ||
!this.dashboardPanelStorage
) {
return;
}
this.dashboardPanelStorage.setPanels(this.savedDashboard?.id, newPanels);
}

private toUrlState(state: DashboardAppState): DashboardAppStateInUrl {
if (this.getIsEditMode() && !this.allowByValueEmbeddables) {
return state;
}
const { panels, ...stateWithoutPanels } = state;
return stateWithoutPanels;
}

private checkIsDirty() {
// Filters need to be compared manually because they sometimes have a $$hashkey stored on the object.
// Query needs to be compared manually because saved legacy queries get migrated in app state automatically
Expand All @@ -653,13 +730,4 @@ export class DashboardStateManager {
const current = _.omit(this.stateContainer.get(), propsToIgnore);
return !_.isEqual(initial, current);
}

private toUrlState(state: DashboardAppState): DashboardAppStateInUrl {
if (state.viewMode === ViewMode.VIEW) {
const { panels, ...stateWithoutPanels } = state;
return stateWithoutPanels;
}

return state;
}
}
Loading