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 1 commit
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
5 changes: 4 additions & 1 deletion 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 { 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;
}
13 changes: 1 addition & 12 deletions src/plugins/wizard/public/application/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*/

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 @@ -13,24 +12,14 @@ 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 savedWizardViz={savedWizardViz} />
<TopNav />
<SideNav />
<Workspace />
</EuiPage>
Expand Down
56 changes: 25 additions & 31 deletions src/plugins/wizard/public/application/components/top_nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,24 @@
* 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 { SavedObject } from '../../../../saved_objects/public';
import { useSavedWizardVis } from '../utils/use/use_saved_wizard_vis';

export const TopNav = ({ savedWizardViz }: { savedWizardViz?: SavedObject }) => {
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 },
},
Expand All @@ -29,34 +30,27 @@ export const TopNav = ({ savedWizardViz }: { savedWizardViz?: SavedObject }) =>
(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 savedWizardVis = useSavedWizardVis(services, visualizationIdFromUrl);

const indexPattern = useIndexPattern();
const config = useMemo(() => {
if (savedWizardVis === undefined) {
return;
}
const { visualization: visualizationState, style: styleState } = rootState;

useEffect(() => {
const visualizeHref = window.location.href.split(`${PLUGIN_ID}#/`)[0] + `${VISUALIZE_ID}#/`;
chrome.setBreadcrumbs([
return getTopNavConfig(
{
text: i18n.translate('visualize.listing.breadcrumb', {
defaultMessage: 'Visualize',
}),
href: visualizeHref,
visualizationIdFromUrl,
savedWizardVis,
visualizationState,
styleState,
hasUnappliedChanges,
},
{
text: i18n.translate('wizard.nav.breadcrumb.create', {
defaultMessage: 'Create',
}),
},
]);
// 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
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,
},
];
}
94 changes: 54 additions & 40 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,64 +37,73 @@ import {
showSaveModal,
} from '../../../../saved_objects/public';
import { WizardServices } from '../..';
import { WIZARD_SAVED_OBJECT } from '../../../common';

import { WizardVisSavedObject } from '../../types';
import { StyleState, VisualizationState } from './state_management';
import { EDIT_PATH } from '../../../common';
interface TopNavConfigParams {
visInstance: Record<string, any>; // TODO: fix this type
visualizationIdFromUrl: string;
savedWizardVis: WizardVisSavedObject;
visualizationState: VisualizationState;
styleState: StyleState;
hasUnappliedChanges: boolean;
}

export const getTopNavconfig = (
{ visInstance, hasUnappliedChanges }: TopNavConfigParams,
export const getTopNavConfig = (
{
savedObjects: { client: savedObjectsClient },
toastNotifications,
i18n: { Context: I18nContext },
}: WizardServices
visualizationIdFromUrl,
savedWizardVis,
visualizationState,
styleState,
hasUnappliedChanges,
}: TopNavConfigParams,
{ history, toastNotifications, i18n: { Context: I18nContext } }: WizardServices
) => {
const { state } = visInstance;
const topNavConfig: TopNavMenuData[] = [
{
id: 'save',
iconType: 'save',
emphasize: true, // TODO: need to be conditional for save vs create (save as)?
description: 'Save Visualization', // TODO: i18n
emphasize: savedWizardVis && !savedWizardVis.id,
description: i18n.translate('wizard.topNavMenu.saveVisualizationButtonAriaLabel', {
defaultMessage: 'Save Visualization',
}),
className: 'saveButton',
label: 'save', // TODO: i18n
label: i18n.translate('wizard.topNavMenu.saveVisualizationButtonLabel', {
defaultMessage: 'save',
}),
testId: 'wizardSaveButton',
disableButton: hasUnappliedChanges,
tooltip() {
if (hasUnappliedChanges) {
return i18n.translate('visualize.topNavMenu.saveVisualizationDisabledButtonTooltip', {
return i18n.translate('wizard.topNavMenu.saveVisualizationDisabledButtonTooltip', {
defaultMessage: 'Apply aggregation configuration changes before saving', // TODO: Update text to match agg save flow
});
}
},
run: (anchorElement) => {
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 = 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,
});
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 @@ -103,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 @@ -119,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 @@ -137,7 +151,7 @@ export const getTopNavconfig = (

const saveModal = (
<SavedObjectSaveModalOrigin
documentInfo={visInstance || { title: '' }}
documentInfo={savedWizardVis}
onSave={onSave}
objectType={'wizard'}
onClose={() => {}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ 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';
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
Loading