Skip to content

Commit

Permalink
[D&D] Enable basic saving and loading
Browse files Browse the repository at this point in the history
- 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

partially addresses opensearch-project#1620

Signed-off-by: Josh Romero <rmerqg@amazon.com>
  • Loading branch information
joshuarrrr committed Jul 8, 2022
1 parent e1f1c49 commit 8e26a4b
Show file tree
Hide file tree
Showing 16 changed files with 212 additions and 38 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';
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { SavedObjectAttributes } from 'opensearch-dashboards/public';
import { SavedObjectAttributes } from '../../../core/types';

export const WIZARD_SAVED_OBJECT = 'wizard';

Expand Down
13 changes: 12 additions & 1 deletion src/plugins/wizard/public/application/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import React from 'react';
import { useParams } from 'react-router-dom';
import { I18nProvider } from '@osd/i18n/react';
import { EuiPage } from '@elastic/eui';
import { SideNav } from './components/side_nav';
Expand All @@ -12,14 +13,24 @@ import { Workspace } from './components/workspace';

import './app.scss';
import { TopNav } from './components/top_nav';
import { useOpenSearchDashboards } from '../../../opensearch_dashboards_react/public';
import { WizardServices } from '../types';
import { useSavedWizardVisInstance } from './utils/use/use_saved_wizard_vis';

export const WizardApp = () => {
const { id: visualizationIdFromUrl } = useParams<{ id: string }>();

const { services } = useOpenSearchDashboards<WizardServices>();

const savedWizardVisInstance = useSavedWizardVisInstance(services, visualizationIdFromUrl);
const savedWizardViz = savedWizardVisInstance?.savedWizardVis;

// Render the application DOM.
return (
<I18nProvider>
<DragDropProvider>
<EuiPage className="wizLayout">
<TopNav />
<TopNav savedWizardViz={savedWizardViz} />
<SideNav />
<Workspace />
</EuiPage>
Expand Down
17 changes: 15 additions & 2 deletions src/plugins/wizard/public/application/components/top_nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import { WizardServices } from '../../types';

import './top_nav.scss';
import { useIndexPattern } from '../utils/use';
import { useTypedSelector } from '../utils/state_management';
import { SavedObject } from '../../../../saved_objects/public';

export const TopNav = () => {
export const TopNav = ({ savedWizardViz }: { savedWizardViz?: SavedObject }) => {
const { services } = useOpenSearchDashboards<WizardServices>();
const {
setHeaderActionMenu,
Expand All @@ -22,8 +24,19 @@ export const TopNav = () => {
ui: { TopNavMenu },
},
} = services;
const rootState = useTypedSelector((state) => state);
const hasUnappliedChanges = useTypedSelector(
(state) => !!state.visualization.activeVisualization?.draftAgg
);

const config = useMemo(() => {
const visInstance = {
...savedWizardViz,
state: JSON.stringify(rootState),
};
return getTopNavconfig({ visInstance, hasUnappliedChanges }, services);
}, [hasUnappliedChanges, rootState, savedWizardViz, services]);

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

useEffect(() => {
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
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;
};
54 changes: 40 additions & 14 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,19 +37,39 @@ import {
showSaveModal,
} from '../../../../saved_objects/public';
import { WizardServices } from '../..';
import { WIZARD_SAVED_OBJECT } from '../../../common';

export const getTopNavconfig = ({
savedObjects: { client: savedObjectsClient },
toastNotifications,
i18n: { Context: I18nContext },
}: WizardServices) => {
interface TopNavConfigParams {
visInstance: Record<string, any>; // TODO: fix this type
hasUnappliedChanges: boolean;
}

export const getTopNavconfig = (
{ visInstance, hasUnappliedChanges }: TopNavConfigParams,
{
savedObjects: { client: savedObjectsClient },
toastNotifications,
i18n: { Context: I18nContext },
}: WizardServices
) => {
const { state } = visInstance;
const topNavConfig: TopNavMenuData[] = [
{
id: 'save',
iconType: 'save',
emphasize: true,
label: 'Save',
emphasize: true, // TODO: need to be conditional for save vs create (save as)?
description: 'Save Visualization', // TODO: i18n
className: 'saveButton',
label: 'save', // TODO: i18n
testId: 'wizardSaveButton',
disableButton: hasUnappliedChanges,
tooltip() {
if (hasUnappliedChanges) {
return i18n.translate('visualize.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
Expand All @@ -61,11 +81,17 @@ export const getTopNavconfig = ({
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({}),
});
const wizardSavedObject = visInstance.id
? await savedObjectsClient.update(WIZARD_SAVED_OBJECT, visInstance.id, {
title: newTitle,
description: newDescription,
state,
})
: await savedObjectsClient.create(WIZARD_SAVED_OBJECT, {
title: newTitle,
description: newDescription,
state,
});

try {
const id = await wizardSavedObject.save();
Expand Down Expand Up @@ -111,9 +137,9 @@ export const getTopNavconfig = ({

const saveModal = (
<SavedObjectSaveModalOrigin
documentInfo={{ title: '' }}
documentInfo={visInstance || { title: '' }}
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 } from './style_slice';
export { setState as setVisualizationState } from './visualization_slice';
Original file line number Diff line number Diff line change
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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

// import { i18n } from '@osd/i18n';
import { useEffect, useState } from 'react';
import { SavedObject } from '../../../../../saved_objects/public';
// import { redirectWhenMissing } from '../../../../../opensearch_dashboards_utils/public';
// import { EDIT_PATH } from '../../../../common';
import { WizardServices } from '../../../types';
import { MetricOptionsDefaults } from '../../../visualizations/metric/metric_viz_type';
import { getSavedWizardVis } from '../get_saved_wizard_vis';
import { useTypedDispatch, setStyleState, setVisualizationState } from '../state_management';

export const useSavedWizardVisInstance = (
services: WizardServices,
visualizationIdFromUrl: string | undefined
) => {
const [state, setState] = useState<{
savedWizardVis?: SavedObject;
}>({});
const dispatch = useTypedDispatch();

useEffect(() => {
const {
// application: { navigateToApp },
chrome,
// history,
// http: { basePath },
// setActiveUrl,
// toastNotifications,
} = services;
// TODO: remove "instance" from naming
const getSavedWizardVisInstance = async () => {
try {
const savedWizardVis = await getSavedWizardVis(services, visualizationIdFromUrl);

if (savedWizardVis.id) {
// TODO: update breadcrumbs
// chrome.setBreadcrumbs(getEditBreadcrumbs(savedWizardVis.title));
chrome.docTitle.change(savedWizardVis.title);
} else {
// chrome.setBreadcrumbs(getCreateBreadcrumbs());
}

dispatch(setStyleState<MetricOptionsDefaults>(JSON.parse(savedWizardVis.state).style));
dispatch(setVisualizationState(JSON.parse(savedWizardVis.state).visualization));
setState({ savedWizardVis });
} catch (error) {
// TODO: implement error handling
// const managementRedirectTarget = {
// app: 'management',
// path: `opensearch-dashboards/objects/savedVisualizations/${visualizationIdFromUrl}`,
// };
//
// try {
// redirectWhenMissing({
// history,
// navigateToApp,
// toastNotifications,
// basePath,
// mapping: managementRedirectTarget,
// onBeforeRedirect() {
// setActiveUrl(EDIT_PATH);
// },
// })(error);
// } catch (e) {
// toastNotifications.addWarning({
// title: i18n.translate('visualize.createVisualization.failedToLoadErrorMessage', {
// defaultMessage: 'Failed to load the visualization',
// }),
// text: e.message,
// });
// history.replace(EDIT_PATH);
// }
}
};

getSavedWizardVisInstance();
}, [dispatch, services, visualizationIdFromUrl]);

return state;
};
5 changes: 3 additions & 2 deletions src/plugins/wizard/public/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { dataPluginMock } from '../../../plugins/data/public/mocks';
import { embeddablePluginMock } from '../../../plugins/embeddable/public/mocks';
import { navigationPluginMock } from '../../../plugins/navigation/public/mocks';
import { visualizationsPluginMock } from '../../../plugins/visualizations/public/mocks';
import { PLUGIN_ID, PLUGIN_NAME } from '../common';
import { WizardPlugin } from './plugin';

describe('WizardPlugin', () => {
Expand All @@ -34,8 +35,8 @@ describe('WizardPlugin', () => {
expect(setupDeps.visualizations.registerAlias).toHaveBeenCalledWith(
// TODO: Update this once the properties are final
expect.objectContaining({
name: 'wizard',
title: 'Wizard',
name: PLUGIN_ID,
title: PLUGIN_NAME,
aliasPath: '#/',
})
);
Expand Down
Loading

0 comments on commit 8e26a4b

Please sign in to comment.