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

[D&D] Basic saving, loading, and updating #1870

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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);
Copy link
Member

@ashwin-pc ashwin-pc Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but to handle a failed save and to reset the saved object, you can just use immers's produce function to clone the object and modify the before saving it. That way the original saved object is untouched. That also keeps savedWizardVis from being mutated.

We already have a dependency on immer for redux-toolkit

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