Skip to content

Commit

Permalink
[D&D] Basic saving, loading, and updating (opensearch-project#1870)
Browse files Browse the repository at this point in the history
* [D&D] Enable basic saving and loading

- Add `/edit` route
- Sync state for saving and loading
- Add setter for vizualization slice
- Switch from BrowserRouter to Router
- Add version to saved objects
- Add savedWizardLoader to services
- store visualization and style states separately
- add version
- update breadcrumb handling
- move useSavedWizardVis to top_nav
- handle savedObjectNotFound
- use savedObjectLoader correctly
- allow copy on save
- update url and chrome on save
- add type for WizardVisSavedObject

fixes opensearch-project#1867

Signed-off-by: Josh Romero <rmerqg@amazon.com>
  • Loading branch information
joshuarrrr authored and kavilla committed Aug 3, 2022
1 parent 1333b33 commit 4b39397
Show file tree
Hide file tree
Showing 17 changed files with 322 additions and 84 deletions.
2 changes: 1 addition & 1 deletion src/plugins/saved_objects_management/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ From the primary UI page, this plugin allows you to:
2. Import/export saved objects
3. Inspect/edit raw saved object values without validation

For 3., this plugin can also be used to provide a route/page for editing, such as `/app/management/opensearch-dashboards/objects/savedVisualizations/{visualizationId}`, although plugins are alos free to provide or host alternate routes for this purpose (see index patterns, for instance, which provide their own integration and UI via the `management` plugin directly).
For 3., this plugin can also be used to provide a route/page for editing, such as `/app/management/opensearch-dashboards/objects/savedVisualizations/{visualizationId}`, although plugins are also free to provide or host alternate routes for this purpose (see index patterns, for instance, which provide their own integration and UI via the `management` plugin directly).
## Making a new saved object type manageable

1. Create a new `SavedObjectsType` or add the `management` property to an existing one. (See `SavedObjectsTypeManagementDefinition` for explanation of its properties: https://github.com/opensearch-project/OpenSearch-Dashboards/blob/e1380f14deb98cc7cce55c3b82c2d501826a78c3/src/core/server/saved_objects/types.ts#L247-L285)
Expand Down
1 change: 1 addition & 0 deletions src/plugins/wizard/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
export const PLUGIN_ID = 'wizard';
export const PLUGIN_NAME = 'Wizard';
export const VISUALIZE_ID = 'visualize';
export const EDIT_PATH = '/edit';

export { WizardSavedObjectAttributes, WIZARD_SAVED_OBJECT } from './wizard_saved_object_attributes';
7 changes: 5 additions & 2 deletions src/plugins/wizard/common/wizard_saved_object_attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { SavedObjectAttributes } from 'opensearch-dashboards/public';
import { integer } from '@opensearch-project/opensearch/api/types';
import { SavedObjectAttributes } from '../../../core/types';

export const WIZARD_SAVED_OBJECT = 'wizard';

export interface WizardSavedObjectAttributes extends SavedObjectAttributes {
title: string;
description?: string;
state: string;
visualizationState?: string;
styleState?: string;
version: integer;
}
53 changes: 30 additions & 23 deletions src/plugins/wizard/public/application/components/top_nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,54 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { i18n } from '@osd/i18n';
import React, { useMemo, useEffect } from 'react';
import { PLUGIN_ID, VISUALIZE_ID } from '../../../common';
import React, { useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { PLUGIN_ID } from '../../../common';
import { useOpenSearchDashboards } from '../../../../opensearch_dashboards_react/public';
import { getTopNavconfig } from '../utils/get_top_nav_config';
import { getTopNavConfig } from '../utils/get_top_nav_config';
import { WizardServices } from '../../types';

import './top_nav.scss';
import { useIndexPattern } from '../utils/use';
import { useTypedSelector } from '../utils/state_management';
import { useSavedWizardVis } from '../utils/use/use_saved_wizard_vis';

export const TopNav = () => {
// id will only be set for the edit route
const { id: visualizationIdFromUrl } = useParams<{ id: string }>();
const { services } = useOpenSearchDashboards<WizardServices>();
const {
setHeaderActionMenu,
chrome,
navigation: {
ui: { TopNavMenu },
},
} = services;
const rootState = useTypedSelector((state) => state);
const hasUnappliedChanges = useTypedSelector(
(state) => !!state.visualization.activeVisualization?.draftAgg
);

const config = useMemo(() => getTopNavconfig(services), [services]);
const indexPattern = useIndexPattern();
const savedWizardVis = useSavedWizardVis(services, visualizationIdFromUrl);

useEffect(() => {
const visualizeHref = window.location.href.split(`${PLUGIN_ID}#/`)[0] + `${VISUALIZE_ID}#/`;
chrome.setBreadcrumbs([
{
text: i18n.translate('visualize.listing.breadcrumb', {
defaultMessage: 'Visualize',
}),
href: visualizeHref,
},
const config = useMemo(() => {
if (savedWizardVis === undefined) {
return;
}
const { visualization: visualizationState, style: styleState } = rootState;

return getTopNavConfig(
{
text: i18n.translate('wizard.nav.breadcrumb.create', {
defaultMessage: 'Create',
}),
visualizationIdFromUrl,
savedWizardVis,
visualizationState,
styleState,
hasUnappliedChanges,
},
]);
// we want to run this hook exactly once, which you do by an empty dep array
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
services
);
}, [hasUnappliedChanges, rootState, savedWizardVis, services, visualizationIdFromUrl]);

const indexPattern = useIndexPattern();

return (
<div className="wizTopNav">
Expand Down
13 changes: 9 additions & 4 deletions src/plugins/wizard/public/application/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,30 @@

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { Router, Route, Switch } from 'react-router-dom';
import { Provider as ReduxProvider } from 'react-redux';
import { Store } from 'redux';
import { AppMountParameters } from '../../../../core/public';
import { WizardServices } from '../types';
import { WizardApp } from './app';
import { OpenSearchDashboardsContextProvider } from '../../../opensearch_dashboards_react/public';
import { EDIT_PATH } from '../../common';

export const renderApp = (
{ appBasePath, element }: AppMountParameters,
{ element, history }: AppMountParameters,
services: WizardServices,
store: Store
) => {
ReactDOM.render(
<Router basename={appBasePath}>
<Router history={history}>
<OpenSearchDashboardsContextProvider services={services}>
<ReduxProvider store={store}>
<services.i18n.Context>
<WizardApp />
<Switch>
<Route path={[`${EDIT_PATH}/:id`, '/']} exact={false}>
<WizardApp />
</Route>
</Switch>
</services.i18n.Context>
</ReduxProvider>
</OpenSearchDashboardsContextProvider>
Expand Down
42 changes: 42 additions & 0 deletions src/plugins/wizard/public/application/utils/breadcrumbs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { i18n } from '@osd/i18n';
import { VISUALIZE_ID } from '../../../common';

const defaultEditText = i18n.translate('wizard.editor.defaultEditBreadcrumbText', {
defaultMessage: 'Edit',
});

export function getVisualizeLandingBreadcrumbs(navigateToApp) {
return [
{
text: i18n.translate('wizard.listing.breadcrumb', {
defaultMessage: 'Visualize',
}),
onClick: () => navigateToApp(VISUALIZE_ID),
},
];
}

export function getCreateBreadcrumbs(navigateToApp) {
return [
...getVisualizeLandingBreadcrumbs(navigateToApp),
{
text: i18n.translate('wizard.editor.createBreadcrumb', {
defaultMessage: 'Create',
}),
},
];
}

export function getEditBreadcrumbs(text: string = defaultEditText, navigateToApp) {
return [
...getVisualizeLandingBreadcrumbs(navigateToApp),
{
text,
},
];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { WizardServices } from '../..';

export const getSavedWizardVis = async (services: WizardServices, wizardVisId?: string) => {
const { savedWizardLoader } = services;
if (!savedWizardLoader) {
return {};
}
const savedWizardVis = await savedWizardLoader.get(wizardVisId);

return savedWizardVis;
};
96 changes: 68 additions & 28 deletions src/plugins/wizard/public/application/utils/get_top_nav_config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,38 +37,73 @@ import {
showSaveModal,
} from '../../../../saved_objects/public';
import { WizardServices } from '../..';
import { WizardVisSavedObject } from '../../types';
import { StyleState, VisualizationState } from './state_management';
import { EDIT_PATH } from '../../../common';
interface TopNavConfigParams {
visualizationIdFromUrl: string;
savedWizardVis: WizardVisSavedObject;
visualizationState: VisualizationState;
styleState: StyleState;
hasUnappliedChanges: boolean;
}

export const getTopNavconfig = ({
savedObjects: { client: savedObjectsClient },
toastNotifications,
i18n: { Context: I18nContext },
}: WizardServices) => {
export const getTopNavConfig = (
{
visualizationIdFromUrl,
savedWizardVis,
visualizationState,
styleState,
hasUnappliedChanges,
}: TopNavConfigParams,
{ history, toastNotifications, i18n: { Context: I18nContext } }: WizardServices
) => {
const topNavConfig: TopNavMenuData[] = [
{
id: 'save',
iconType: 'save',
emphasize: true,
label: 'Save',
emphasize: savedWizardVis && !savedWizardVis.id,
description: i18n.translate('wizard.topNavMenu.saveVisualizationButtonAriaLabel', {
defaultMessage: 'Save Visualization',
}),
className: 'saveButton',
label: i18n.translate('wizard.topNavMenu.saveVisualizationButtonLabel', {
defaultMessage: 'save',
}),
testId: 'wizardSaveButton',
run: (anchorElement) => {
disableButton: hasUnappliedChanges,
tooltip() {
if (hasUnappliedChanges) {
return i18n.translate('wizard.topNavMenu.saveVisualizationDisabledButtonTooltip', {
defaultMessage: 'Apply aggregation configuration changes before saving', // TODO: Update text to match agg save flow
});
}
},
run: (_anchorElement) => {
const onSave = async ({
// TODO: Figure out what the other props here do
newTitle,
newCopyOnSave,
isTitleDuplicateConfirmed,
onTitleDuplicate,
newDescription,
returnToOrigin,
}: OnSaveProps & { returnToOrigin: boolean }) => {
// TODO: Save the actual state of the wizard
const wizardSavedObject = await savedObjectsClient.create('wizard', {
title: newTitle,
description: newDescription,
state: JSON.stringify({}),
});
if (!savedWizardVis) {
return;
}
savedWizardVis.visualizationState = JSON.stringify(visualizationState);
savedWizardVis.styleState = JSON.stringify(styleState);
savedWizardVis.title = newTitle;
savedWizardVis.description = newDescription;
savedWizardVis.copyOnSave = newCopyOnSave;

try {
const id = await wizardSavedObject.save();
const id = await savedWizardVis.save({
confirmOverwrite: false,
isTitleDuplicateConfirmed,
onTitleDuplicate,
returnToOrigin,
});

if (id) {
toastNotifications.addSuccess({
Expand All @@ -77,13 +112,21 @@ export const getTopNavconfig = ({
{
defaultMessage: `Saved '{visTitle}'`,
values: {
visTitle: newTitle,
visTitle: savedWizardVis.title,
},
}
),
'data-test-subj': 'saveVisualizationSuccess',
});

// Update URL
if (id !== visualizationIdFromUrl) {
history.push({
...history.location,
pathname: `${EDIT_PATH}/${id}`,
});
}

return { id };
}

Expand All @@ -93,15 +136,12 @@ export const getTopNavconfig = ({
console.error(error);

toastNotifications.addDanger({
title: i18n.translate(
'visualize.topNavMenu.saveVisualization.failureNotificationText',
{
defaultMessage: `Error on saving '{visTitle}'`,
values: {
visTitle: newTitle,
},
}
),
title: i18n.translate('wizard.topNavMenu.saveVisualization.failureNotificationText', {
defaultMessage: `Error on saving '{visTitle}'`,
values: {
visTitle: newTitle,
},
}),
text: error.message,
'data-test-subj': 'saveVisualizationError',
});
Expand All @@ -111,9 +151,9 @@ export const getTopNavconfig = ({

const saveModal = (
<SavedObjectSaveModalOrigin
documentInfo={{ title: '' }}
documentInfo={savedWizardVis}
onSave={onSave}
objectType={'visualization'}
objectType={'wizard'}
onClose={() => {}}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ export const getPreloadedStore = async (services: WizardServices) => {
export type RootState = ReturnType<typeof rootReducer>;
type Store = ReturnType<typeof configurePreloadedStore>;
export type AppDispatch = Store['dispatch'];

export { setState as setStyleState, StyleState } from './style_slice';
export { setState as setVisualizationState, VisualizationState } from './visualization_slice';
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { WizardServices } from '../../../types';

type StyleState<T = any> = T;
export type StyleState<T = any> = T;

const initialState = {} as StyleState;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { CreateAggConfigParams } from '../../../../../data/common';
import { WizardServices } from '../../../types';

interface VisualizationState {
export interface VisualizationState {
indexPattern?: string;
searchField: string;
activeVisualization?: {
Expand Down Expand Up @@ -105,6 +105,9 @@ export const slice = createSlice({
updateAggConfigParams: (state, action: PayloadAction<CreateAggConfigParams[]>) => {
state.activeVisualization!.aggConfigParams = action.payload;
},
setState: (_state, action: PayloadAction<VisualizationState>) => {
return action.payload;
},
},
});

Expand All @@ -117,4 +120,5 @@ export const {
updateAggConfigParams,
saveAgg,
reorderAgg,
setState,
} = slice.actions;
Loading

0 comments on commit 4b39397

Please sign in to comment.