diff --git a/.cypress/integration/3_panels.spec.ts b/.cypress/integration/3_panels.spec.ts index 252be657a..87a4ed902 100644 --- a/.cypress/integration/3_panels.spec.ts +++ b/.cypress/integration/3_panels.spec.ts @@ -644,8 +644,7 @@ const eraseTestPanels = () => { eraseLegacyPanels(); eraseSavedObjectPaenls(); }; -const uuidRx = - /[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/; +const uuidRx = /[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/; const clickCreatePanelButton = () => cy.get('a[data-test-subj="customPanels__createNewPanels"]').click(); diff --git a/common/constants/custom_panels.ts b/common/constants/custom_panels.ts index 1062d63a2..f0791d08c 100644 --- a/common/constants/custom_panels.ts +++ b/common/constants/custom_panels.ts @@ -6,7 +6,7 @@ export const CUSTOM_PANELS_API_PREFIX = '/api/observability/operational_panels'; export const CUSTOM_PANELS_DOCUMENTATION_URL = 'https://opensearch.org/docs/latest/observability-plugin/operational-panels/'; -export const CREATE_PANEL_MESSAGE = 'Enter a name to describe the purpose of this custom panel.'; +export const CREATE_PANEL_MESSAGE = 'Enter a name to describe the purpose of this Observability Dashboard.'; export const CUSTOM_PANELS_SAVED_OBJECT_TYPE = 'observability-panel'; diff --git a/common/constants/shared.ts b/common/constants/shared.ts index 579799460..e42bd11c6 100644 --- a/common/constants/shared.ts +++ b/common/constants/shared.ts @@ -25,7 +25,7 @@ export const DSL_ENDPOINT = '/_plugins/_dsl'; export const observabilityID = 'observability-logs'; export const observabilityTitle = 'Observability'; -export const observabilityPluginOrder = 6000; +export const observabilityPluginOrder = 1500; export const observabilityApplicationsID = 'observability-applications'; export const observabilityApplicationsTitle = 'Applications'; diff --git a/public/components/common/toast/index.tsx b/public/components/common/toast/index.tsx new file mode 100644 index 000000000..6eaef004c --- /dev/null +++ b/public/components/common/toast/index.tsx @@ -0,0 +1,37 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ToastInputFields } from '../../../../../../src/core/public'; +import { coreRefs } from '../../../framework/core_refs'; + +type Color = 'success' | 'primary' | 'warning' | 'danger' | undefined; + +export const useToast = () => { + const toasts = coreRefs.toasts!; + + const setToast = (title: string, color: Color = 'success', text?: string) => { + const newToast: ToastInputFields = { + id: new Date().toISOString(), + title, + text, + }; + switch (color) { + case 'danger': { + toasts.addDanger(newToast); + break; + } + case 'warning': { + toasts.addWarning(newToast); + break; + } + default: { + toasts.addSuccess(newToast); + break; + } + } + }; + + return { setToast }; +}; diff --git a/public/components/custom_panels/__tests__/custom_panel_view.test.tsx b/public/components/custom_panels/__tests__/custom_panel_view.test.tsx index b37601519..d4eb276e6 100644 --- a/public/components/custom_panels/__tests__/custom_panel_view.test.tsx +++ b/public/components/custom_panels/__tests__/custom_panel_view.test.tsx @@ -20,13 +20,15 @@ import PPLService from '../../../../public/services/requests/ppl'; import DSLService from '../../../../public/services/requests/dsl'; import { coreStartMock } from '../../../../test/__mocks__/coreMocks'; import { HttpResponse } from '../../../../../../src/core/public'; -import { createStore } from '@reduxjs/toolkit'; +import { applyMiddleware, createStore } from 'redux'; import { rootReducer } from '../../../framework/redux/reducers'; +import thunk from 'redux-thunk'; import { Provider } from 'react-redux'; describe('Panels View Component', () => { configure({ adapter: new Adapter() }); - const store = createStore(rootReducer); + + const store = createStore(rootReducer, applyMiddleware(thunk)); it('renders panel view container without visualizations', async () => { httpClientMock.get = jest.fn(() => diff --git a/public/components/custom_panels/custom_panel_table.tsx b/public/components/custom_panels/custom_panel_table.tsx index cf4273de6..4b1fd571d 100644 --- a/public/components/custom_panels/custom_panel_table.tsx +++ b/public/components/custom_panels/custom_panel_table.tsx @@ -48,7 +48,6 @@ import { getSampleDataModal } from '../common/helpers/add_sample_modal'; import { pageStyles } from '../../../common/constants/shared'; import { DeleteModal } from '../common/helpers/delete_modal'; import { - clonePanel, createPanel, deletePanels, fetchPanels, @@ -57,6 +56,8 @@ import { renameCustomPanel, selectPanelList, } from './redux/panel_slice'; +import { isNameValid } from './helpers/utils'; +import { useToast } from '../common/toast'; /* * "CustomPanelTable" module, used to view all the saved panels @@ -77,8 +78,6 @@ interface Props { loading: boolean; setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; parentBreadcrumbs: EuiBreadcrumb[]; - cloneCustomPanel: (newCustomPanelName: string, customPanelId: string) => void; - deleteCustomPanelList: (customPanelIdList: string[], toastMessage: string) => any; addSamplePanels: () => void; } @@ -86,8 +85,6 @@ export const CustomPanelTable = ({ loading, setBreadcrumbs, parentBreadcrumbs, - cloneCustomPanel, - deleteCustomPanelList, addSamplePanels, }: Props) => { const customPanels = useSelector(selectPanelList); @@ -100,16 +97,13 @@ export const CustomPanelTable = ({ const history = useHistory(); const dispatch = useDispatch(); + const { setToast } = useToast(); useEffect(() => { setBreadcrumbs(parentBreadcrumbs); dispatch(fetchPanels()); }, []); - // useEffect(() => - // console.log({ customPanels, selectedCustomPanels }, [customPanels, selectedCustomPanels]) - // ); - useEffect(() => { const url = window.location.hash.split('/'); if (url[url.length - 1] === 'create') { @@ -126,55 +120,58 @@ export const CustomPanelTable = ({ }; const onCreate = async (newCustomPanelName: string) => { - const newPanel = newPanelTemplate(newCustomPanelName); - dispatch(createPanel(newPanel)); + if (!isNameValid(newCustomPanelName)) { + setToast('Invalid Dashboard name', 'danger'); + } else { + const newPanel = newPanelTemplate(newCustomPanelName); + dispatch(createPanel(newPanel)); + } closeModal(); }; const onRename = async (newCustomPanelName: string) => { - dispatch(renameCustomPanel(newCustomPanelName, selectedCustomPanels[0].id)); + if (!isNameValid(newCustomPanelName)) { + setToast('Invalid Dashboard name', 'danger'); + } else { + dispatch(renameCustomPanel(newCustomPanelName, selectedCustomPanels[0].id)); + } closeModal(); }; const onClone = async (newName: string) => { - let sourcePanel = selectedCustomPanels[0]; - try { - if (!isUuid(sourcePanel.id)) { - // Observability Panel API returns partial record, so for duplication - // we will retrieve the entire record and allow new process to continue. - const legacyFetchResult = await coreRefs.http!.get( - `${CUSTOM_PANELS_API_PREFIX}/panels/${sourcePanel.id}` - ); - sourcePanel = legacyFetchResult.operationalPanel; - } + if (!isNameValid(newName)) { + setToast('Invalid Operational Panel name', 'danger'); + } else { + let sourcePanel = selectedCustomPanels[0]; + try { + if (!isUuid(sourcePanel.id)) { + // Observability Panel API returns partial record, so for duplication + // we will retrieve the entire record and allow new process to continue. + const legacyFetchResult = await coreRefs.http!.get( + `${CUSTOM_PANELS_API_PREFIX}/panels/${sourcePanel.id}` + ); + sourcePanel = legacyFetchResult.operationalPanel; + } - const { id, ...newPanel } = { - ...sourcePanel, - title: newName, - }; + const { id, ...newPanel } = { + ...sourcePanel, + title: newName, + }; - dispatch(createPanel(newPanel)); - } catch (err) { - console.log(err); + dispatch(createPanel(newPanel)); + } catch (err) { + setToast( + 'Error cloning Observability Dashboard, please make sure you have the correct permission.', + 'danger' + ); + console.error(err); + } } closeModal(); }; const onDelete = async () => { - const toastMessage = `Observability Dashboards ${ - selectedCustomPanels.length > 1 ? 's' : ' ' + selectedCustomPanels[0].title - } successfully deleted!`; - - try { - dispatch(deletePanels(selectedCustomPanels)); - } catch (err) { - // setToast( - // 'Error deleting Operational Panels, please make sure you have the correct permission.', - // 'danger' - // ); - console.error(err.body?.message || err); - } - + dispatch(deletePanels(selectedCustomPanels)); closeModal(); }; @@ -337,7 +334,6 @@ export const CustomPanelTable = ({ }, ] as Array>; - // console.log('rendering', { customPanels, selectedCustomPanels }); return (
diff --git a/public/components/custom_panels/custom_panel_view.tsx b/public/components/custom_panels/custom_panel_view.tsx index 2384d82ef..fd1b35d12 100644 --- a/public/components/custom_panels/custom_panel_view.tsx +++ b/public/components/custom_panels/custom_panel_view.tsx @@ -70,6 +70,7 @@ import { AddVisualizationPopover } from './helpers/add_visualization_popover'; import { DeleteModal } from '../common/helpers/delete_modal'; import { coreRefs } from '../../framework/core_refs'; import { clonePanel } from './redux/panel_slice'; +import { useToast } from '../common/toast'; /* * "CustomPanelsView" module used to render an Observability Dashboard @@ -104,12 +105,6 @@ interface CustomPanelViewProps { chrome: CoreStart['chrome']; parentBreadcrumbs: EuiBreadcrumb[]; cloneCustomPanel: (clonedCustomPanelName: string, clonedCustomPanelId: string) => Promise; - setToast: ( - title: string, - color?: string, - text?: React.ReactChild | undefined, - side?: string | undefined - ) => void; onEditClick: (savedVisualizationId: string) => any; startTime: string; endTime: string; @@ -138,7 +133,6 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { setEndTime, updateAvailabilityVizId, cloneCustomPanel, - setToast, onEditClick, onAddClick, } = props; @@ -169,6 +163,8 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { const dispatch = useDispatch(); + const { setToast } = useToast(); + const closeHelpFlyout = () => { setAddVizDisabled(false); setHelpIsFlyoutVisible(false); @@ -318,24 +314,11 @@ export const CustomPanelView = (props: CustomPanelViewProps) => { }; const onClone = async (newCustomPanelName: string) => { - try { - await dispatch(clonePanel(panel, newCustomPanelName)); - } catch (err) { - setToast('Error while attempting to Duplicate this Dashboard.', 'danger'); + if (!isNameValid(newCustomPanelName)) { + setToast('Invalid Operational Panel name', 'danger'); + } else { + dispatch(clonePanel(panel, newCustomPanelName)); } - - // const newPanel = { - // ...panel, - // title: newCustomPanelName, - // dateCreated: new Date().getTime(), - // dateModified: new Date().getTime(), - // } as PanelType; - // const newSOPanel = await coreRefs.savedObjectsClient!.create( - // CUSTOM_PANELS_SAVED_OBJECT_TYPE, - // newPanel - // ); - // - // window.location.assign(`${last(parentBreadcrumbs)!.href}${newSOPanel.id}`); closeModal(); }; diff --git a/public/components/custom_panels/custom_panel_view_so.tsx b/public/components/custom_panels/custom_panel_view_so.tsx index d1dd4f1bf..65b10416a 100644 --- a/public/components/custom_panels/custom_panel_view_so.tsx +++ b/public/components/custom_panels/custom_panel_view_so.tsx @@ -47,6 +47,7 @@ import { getCustomModal } from './helpers/modal_containers'; import { convertDateTime, isDateValid, + isNameValid, isPPLFilterValid, prependRecentlyUsedRange, } from './helpers/utils'; @@ -57,13 +58,15 @@ import { clonePanel, deletePanels, fetchPanel, + renameCustomPanel, selectPanel, setPanel, updatePanel, } from './redux/panel_slice'; +import { useToast } from '../common/toast'; import PPLService from '../../services/requests/ppl'; import DSLService from '../../services/requests/dsl'; - + /* * "CustomPanelsView" module used to render an Observability Dashboard * @@ -79,7 +82,6 @@ import DSLService from '../../services/requests/dsl'; * renameCustomPanel: Rename function for the panel * deleteCustomPanel: Delete function for the panel * cloneCustomPanel: Clone function for the panel - * setToast: create Toast function * onEditClick: Edit function for visualization * startTime: Starting time * endTime: Ending time @@ -97,12 +99,6 @@ interface CustomPanelViewProps { chrome: CoreStart['chrome']; parentBreadcrumbs: EuiBreadcrumb[]; cloneCustomPanel: (clonedCustomPanelName: string, clonedCustomPanelId: string) => Promise; - setToast: ( - title: string, - color?: string, - text?: React.ReactChild | undefined, - side?: string | undefined - ) => void; onEditClick: (savedVisualizationId: string) => any; childBreadcrumbs?: EuiBreadcrumb[]; appId?: string; @@ -124,12 +120,12 @@ export const CustomPanelViewSO = (props: CustomPanelViewProps) => { childBreadcrumbs, updateAvailabilityVizId, cloneCustomPanel, - setToast, onEditClick, onAddClick, } = props; const dispatch = useDispatch(); + const { setToast } = useToast(); const panel = useSelector(selectPanel); const [loading, setLoading] = useState(true); @@ -185,27 +181,17 @@ export const CustomPanelViewSO = (props: CustomPanelViewProps) => { timeProps.end, recentlyUsedRanges ); - dispatch(updatePanel({ ...panel, timeRange: { from: timeProps.start, to: timeProps.end } })); + dispatch(updatePanel({ ...panel, timeRange: { from: timeProps.start, to: timeProps.end } }, '', '')); setRecentlyUsedRanges(updatedRanges.slice(0, 9)); onRefreshFilters(timeProps.start, timeProps.end); }; const onDelete = async () => { - const toastMessage = `Observability Dashboard ${panel.title} successfully deleted!"`; - try { - await dispatch(deletePanels([panel])); - - setTimeout(() => { - window.location.assign(`${last(parentBreadcrumbs)!.href}`); - }, 1000); - } catch (err) { - setToast( - 'Error deleting Operational Panels, please make sure you have the correct permission.', - 'danger' - ); - console.error(err.body?.message || err); - } + dispatch(deletePanels([panel])); + setTimeout(() => { + window.location.assign(`${last(parentBreadcrumbs)!.href}`); + }, 1000); closeModal(); }; @@ -222,16 +208,10 @@ export const CustomPanelViewSO = (props: CustomPanelViewProps) => { }; const onRename = async (newCustomPanelName: string) => { - const newPanel = { ...panel, title: newCustomPanelName }; - try { - dispatch(updatePanel(newPanel)); - setToast(`Operational Panel successfully renamed into "${newCustomPanelName}"`); - } catch (err) { - setToast( - 'Error renaming Operational Panel, please make sure you have the correct permission.', - 'danger' - ); - console.error(err.body.message); + if (!isNameValid(newCustomPanelName)) { + setToast('Invalid Dashboard name', 'danger'); + } else { + dispatch(renameCustomPanel(newCustomPanelName, panel.id)); } closeModal(); }; @@ -253,7 +233,11 @@ export const CustomPanelViewSO = (props: CustomPanelViewProps) => { }; const onClone = async (newCustomPanelName: string) => { - dispatch(clonePanel(panel, newCustomPanelName)); + if (!isNameValid(newCustomPanelName)) { + setToast('Invalid Operational Panel name', 'danger'); + } else { + dispatch(clonePanel(panel, newCustomPanelName)); + } closeModal(); }; @@ -350,7 +334,7 @@ export const CustomPanelViewSO = (props: CustomPanelViewProps) => { } if (!isPPLFilterValid(pplFilterValue, setToast)) { - console.log(pplFilterValue); + console.error(pplFilterValue); return; } @@ -370,22 +354,7 @@ export const CustomPanelViewSO = (props: CustomPanelViewProps) => { }; const cloneVisualization = (visualzationTitle: string, savedVisualizationId: string) => { - addVisualizationToCurrentPanel({ savedVisualizationId }); - // http - // .post(`${CUSTOM_PANELS_API_PREFIX}/visualizations`, { - // body: JSON.stringify({ - // panelId, - // savedVisualizationId, - // }), - // }) - // .then(async (res) => { - // setPanelVisualizations(res.visualizations); - // setToast(`Visualization ${visualzationTitle} successfully added!`, 'success'); - // }) - // .catch((err) => { - // setToast(`Error in adding ${visualzationTitle} visualization to the panel`, 'danger'); - // console.error(err); - // }); + addVisualizationToCurrentPanel({ savedVisualizationId, successMsg: `Visualization ${visualzationTitle} successfully added!`, failureMsg: `Error in adding ${visualzationTitle} visualization to the panel` }); }; const cancelButton = ( @@ -443,9 +412,13 @@ export const CustomPanelViewSO = (props: CustomPanelViewProps) => { const addVisualizationToCurrentPanel = async ({ savedVisualizationId, oldVisualizationId, + successMsg, + failureMsg, }: { savedVisualizationId: string; oldVisualizationId?: string; + successMsg: string; + failureMsg: string; }) => { const allVisualizations = panel!.visualizations; @@ -456,12 +429,7 @@ export const CustomPanelViewSO = (props: CustomPanelViewProps) => { ); const updatedPanel = { ...panel, visualizations: visualizationsWithNewPanel }; - try { - dispatch(updatePanel(updatedPanel)); - } catch (err) { - setToast('Error adding visualization to this Dashboard', 'danger'); - console.error(err?.body?.message || err); - } + dispatch(updatePanel(updatedPanel, successMsg, failureMsg)); }; const setPanelVisualizations = (newVis) => { @@ -478,7 +446,6 @@ export const CustomPanelViewSO = (props: CustomPanelViewProps) => { pplFilterValue={pplFilterValue} start={panel.timeRange.from} end={panel.timeRange.to} - setToast={setToast} http={coreRefs.http!} pplService={pplService} setPanelVisualizations={setPanelVisualizations} diff --git a/public/components/custom_panels/home.tsx b/public/components/custom_panels/home.tsx index bc75fb6c4..69e94d61e 100644 --- a/public/components/custom_panels/home.tsx +++ b/public/components/custom_panels/home.tsx @@ -3,9 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { EuiBreadcrumb, EuiGlobalToastList, ShortDate, htmlIdGenerator } from '@elastic/eui'; -import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; -import React, { ReactChild, useState } from 'react'; +import { EuiBreadcrumb, ShortDate, htmlIdGenerator } from '@elastic/eui'; +import React, { useState } from 'react'; import { useDispatch, batch } from 'react-redux'; // eslint-disable-next-line @osd/eslint/module_migration import { StaticContext } from 'react-router'; @@ -31,6 +30,8 @@ import { init as initPatterns } from '../event_analytics/redux/slices/patterns_s import { init as initQueryResult } from '../event_analytics/redux/slices/query_result_slice'; import { changeQuery, init as initQuery } from '../event_analytics/redux/slices/query_slice'; import { addTab, setSelectedQueryTab } from '../event_analytics/redux/slices/query_tab_slice'; +import { useToast } from '../common/toast'; +import { coreRefs } from '../../framework/core_refs'; // import { ObjectFetcher } from '../common/objectFetcher'; @@ -67,14 +68,14 @@ export const Home = ({ coreSavedObjects, setBreadcrumbs, }: PanelHomeProps) => { - const [toasts, setToasts] = useState([]); const [loading, setLoading] = useState(false); - const [toastRightSide, setToastRightSide] = useState(true); const [start, setStart] = useState(''); const [end, setEnd] = useState(''); const dispatch = useDispatch(); + const { setToast } = useToast(); + const customPanelBreadCrumbs = [ ...parentBreadcrumbs, { @@ -83,12 +84,6 @@ export const Home = ({ }, ]; - const setToast = (title: string, color = 'success', text?: ReactChild, side?: string) => { - if (!text) text = ''; - setToastRightSide(!side ? true : false); - setToasts([...toasts, { id: new Date().toISOString(), title, text, color } as Toast]); - }; - const addNewTab = async () => { // get a new tabId const tabId = htmlIdGenerator(TAB_ID_TXT_PFX)(); @@ -176,14 +171,6 @@ export const Home = ({ return ( - { - setToasts(toasts.filter((toast) => toast.id !== removedToast.id)); - }} - side={toastRightSide ? 'right' : 'left'} - toastLifeTimeMs={6000} - /> { - console.log('addVisualization Replacement', res); setPanelVisualizations(res.visualizations); setToast(`Visualization ${newVisualizationTitle} successfully added!`, 'success'); }) @@ -202,7 +201,6 @@ export const VisaulizationFlyout = ({ }), }) .then(async (res) => { - console.log('addVisualization New', res); setPanelVisualizations(res.visualizations); setToast(`Visualization ${newVisualizationTitle} successfully added!`, 'success'); }) diff --git a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout_so.tsx b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout_so.tsx index f78679a25..0b1fff5f7 100644 --- a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout_so.tsx +++ b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout_so.tsx @@ -58,6 +58,7 @@ import { } from '../../helpers/utils'; import { replaceVizInPanel, selectPanel } from '../../redux/panel_slice'; import './visualization_flyout.scss'; +import { useToast } from '../../../common/toast'; /* * VisaulizationFlyoutSO - This module create a flyout to add visualization for SavedObjects custom Panels @@ -67,7 +68,6 @@ import './visualization_flyout.scss'; * closeFlyout: function to close the flyout * start: start time in date filter * end: end time in date filter - * setToast: function to set toast in the panel * savedObjects: savedObjects core service * pplService: ppl requestor service * setPanelVisualizations: function set the visualization list in panel @@ -82,12 +82,6 @@ interface VisualizationFlyoutSOProps { start: ShortDate; end: ShortDate; http: CoreStart['http']; - setToast: ( - title: string, - color?: string, - text?: React.ReactChild | undefined, - side?: string | undefined - ) => void; savedObjects: CoreStart['savedObjects']; pplService: PPLService; setPanelVisualizations: React.Dispatch>; @@ -98,22 +92,18 @@ interface VisualizationFlyoutSOProps { } export const VisaulizationFlyoutSO = ({ - panelId, appId = '', pplFilterValue, closeFlyout, start, end, - http, - setToast, - savedObjects, pplService, - setPanelVisualizations, isFlyoutReplacement, replaceVisualizationId, addVisualizationPanel, }: VisualizationFlyoutSOProps) => { const dispatch = useDispatch(); + const { setToast } = useToast(); const panel = useSelector(selectPanel); @@ -167,12 +157,12 @@ export const VisaulizationFlyoutSO = ({ }; const isInputValid = () => { - if (!isDateValid(convertDateTime(start), convertDateTime(end, false), setToast, 'left')) { + if (!isDateValid(convertDateTime(start), convertDateTime(end, false), setToast)) { return false; } if (selectValue === '') { - setToast('Please make a valid selection', 'danger', undefined, 'left'); + setToast('Please make a valid selection', 'danger', undefined); return false; } @@ -183,11 +173,13 @@ export const VisaulizationFlyoutSO = ({ if (!isInputValid()) return; if (isFlyoutReplacement) { - dispatch(replaceVizInPanel(panel, replaceVisualizationId, selectValue)); + dispatch(replaceVizInPanel(panel, replaceVisualizationId, selectValue, newVisualizationTitle)); } else { - const visualizationsWithNewPanel = addVisualizationPanel({ - savedVisualizationId: selectValue, - }); + const visualizationsWithNewPanel = addVisualizationPanel({ + savedVisualizationId: selectValue, + onSuccess: `Visualization ${newVisualizationTitle} successfully added!`, + onFailure: `Error in adding ${newVisualizationTitle} visualization to the panel` + }); } closeFlyout(); }; diff --git a/public/components/custom_panels/redux/panel_slice.ts b/public/components/custom_panels/redux/panel_slice.ts index 684f5f9b7..ae5f26a06 100644 --- a/public/components/custom_panels/redux/panel_slice.ts +++ b/public/components/custom_panels/redux/panel_slice.ts @@ -26,6 +26,7 @@ import { addMultipleVisualizations, addVisualizationPanel, } from '../helpers/add_visualization_helper'; +import { useToast } from '../../../../public/components/common/toast'; interface InitialState { id: string; @@ -81,6 +82,8 @@ const normalizedPanel = (panel: CustomPanelType): CustomPanelType => ({ export const selectPanelList = (rootState): CustomPanelType[] => rootState.customPanel.panelList; +const {setToast} = useToast(); + /* ** ASYNC DISPATCH FUNCTIONS */ @@ -136,16 +139,21 @@ export const uuidRx = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a export const isUuid = (id) => !!id.match(uuidRx); -export const updatePanel = (panel: CustomPanelType) => async (dispatch, getState) => { +export const updatePanel = (panel: CustomPanelType, successMsg: string, failureMsg: string) => async (dispatch, getState) => { try { if (isUuid(panel.id)) await updateSavedObjectPanel(panel); else await updateLegacyPanel(panel); - + if (successMsg) { + setToast(successMsg) + } dispatch(setPanel(panel)); const panelList = getState().customPanel.panelList.map((p) => (p.id === panel.id ? panel : p)); dispatch(setPanelList(panelList)); - } catch (err) { - console.log('Error updating Dashboard', { err, panel }); + } catch (e) { + if (failureMsg) { + setToast(failureMsg, 'danger') + } + console.error(e); } }; @@ -158,11 +166,7 @@ export const addVizToPanels = (panels, vizId) => async (dispatch, getState) => { const visualizationsWithNewPanel = addVisualizationPanel(vizId, undefined, allVisualizations); const updatedPanel = { ...panel, visualizations: visualizationsWithNewPanel }; - try { - dispatch(updatePanel(updatedPanel)); - } catch (err) { - console.error(err?.body?.message || err); - } + dispatch(updatePanel(updatedPanel, '', '')); }); }; @@ -175,15 +179,11 @@ export const addMultipleVizToPanels = (panels, vizIds) => async (dispatch, getSt const visualizationsWithNewPanel = addMultipleVisualizations(vizIds, allVisualizations); const updatedPanel = { ...panel, visualizations: visualizationsWithNewPanel }; - try { - dispatch(updatePanel(updatedPanel)); - } catch (err) { - console.error(err?.body?.message || err); - } + dispatch(updatePanel(updatedPanel, '', '')); }); }; -export const replaceVizInPanel = (oldPanel, oldVizId, vizId) => async (dispatch, getState) => { +export const replaceVizInPanel = (oldPanel, oldVizId, vizId, newVisualizationTitle) => async (dispatch, getState) => { const panel = getState().customPanel.panelList.find((p) => p.id === oldPanel.id); const allVisualizations = panel!.visualizations; @@ -191,11 +191,8 @@ export const replaceVizInPanel = (oldPanel, oldVizId, vizId) => async (dispatch, const visualizationsWithNewPanel = addVisualizationPanel(vizId, oldVizId, allVisualizations); const updatedPanel = { ...panel, visualizations: visualizationsWithNewPanel }; - try { - dispatch(updatePanel(updatedPanel)); - } catch (err) { - console.error(err?.body?.message || err); - } + + dispatch(updatePanel(updatedPanel, `Visualization ${newVisualizationTitle} successfully added!`, `Error in adding ${newVisualizationTitle} visualization to the panel`)); }; const deletePanelSO = (customPanelIdList: string[]) => { @@ -212,40 +209,68 @@ const deleteLegacyPanels = (customPanelIdList: string[]) => { }; export const deletePanels = (panelsToDelete: CustomPanelType[]) => async (dispatch, getState) => { - const ids = panelsToDelete.map((p) => p.id); - await Promise.all([deleteLegacyPanels(ids), deletePanelSO(ids)]); + const toastMessage = `Observability Dashboard${ + panelsToDelete.length > 1 ? 's' : ' ' + panelsToDelete[0].title + } successfully deleted!`; + try { + const ids = panelsToDelete.map((p) => p.id); + await Promise.all([deleteLegacyPanels(ids), deletePanelSO(ids)]); - const panelList: CustomPanelType[] = getState().customPanel.panelList.filter( - (p) => !ids.includes(p.id) - ); - dispatch(setPanelList(panelList)); + const panelList: CustomPanelType[] = getState().customPanel.panelList.filter( + (p) => !ids.includes(p.id) + ); + dispatch(setPanelList(panelList)); + setToast(toastMessage); + } catch (e) { + setToast( + 'Error deleting Observability Dashboards, please make sure you have the correct permission.', + 'danger' + ); + console.error(e); + } }; export const createPanel = (panel) => async (dispatch, getState) => { - const newSOPanel = await savedObjectPanelsClient.create(panel); - const newPanel = savedObjectToCustomPanel(newSOPanel); - const panelList = getState().customPanel.panelList; - dispatch(setPanelList([...panelList, newPanel])); - - window.location.replace(`#/${newPanel.id}`); + try { + const newSOPanel = await savedObjectPanelsClient.create(panel); + const newPanel = savedObjectToCustomPanel(newSOPanel); + const panelList = getState().customPanel.panelList; + dispatch(setPanelList([...panelList, newPanel])); + setToast(`Observability Dashboard "${newPanel.title}" successfully created!`); + window.location.replace(`#/${newPanel.id}`); + } catch (e) { + setToast( + 'Error occurred while creating Observability Dashboard, please make sure you have the correct permission.', + 'danger' + ); + console.error(e); + } }; export const clonePanel = (panel, newPanelName) => async (dispatch, getState) => { - const { id, ...panelCopy } = { - ...panel, - title: newPanelName, - dateCreated: new Date().getTime(), - dateModified: new Date().getTime(), - } as PanelType; - - const newSOPanel = await savedObjectPanelsClient.create(panelCopy); - - const newPanel = savedObjectToCustomPanel(newSOPanel); - const panelList = getState().customPanel.panelList; - dispatch(setPanelList([...panelList, newPanel])); - dispatch(setPanel(newPanel)); - - window.location.replace(`#/${newPanel.id}`); + try { + const { id, ...panelCopy } = { + ...panel, + title: newPanelName, + dateCreated: new Date().getTime(), + dateModified: new Date().getTime(), + } as PanelType; + + const newSOPanel = await savedObjectPanelsClient.create(panelCopy); + + const newPanel = savedObjectToCustomPanel(newSOPanel); + const panelList = getState().customPanel.panelList; + dispatch(setPanelList([...panelList, newPanel])); + dispatch(setPanel(newPanel)); + setToast(`Observability Dashboard "${newPanel.title}" successfully created!`); + window.location.replace(`#/${newPanel.id}`); + } catch (e) { + setToast( + 'Error cloning Observability Dashboard, please make sure you have the correct permission.', + 'danger' + ); + console.error(e); + } }; const saveRenamedPanel = async (id, name) => { @@ -273,34 +298,9 @@ export const renameCustomPanel = (editedCustomPanelName: string, id: string) => dispatch, getState ) => { - if (!isNameValid(editedCustomPanelName)) { - console.log('Invalid Observability Dashboard name', 'danger'); - return Promise.reject(); - } - const panel = getState().customPanel.panelList.find((p) => p.id === id); const updatedPanel = { ...panel, title: editedCustomPanelName }; - dispatch(updatePanel(updatedPanel)); - - // try { - // // await savePanelFn(editedCustomPanelId, editedCustomPanelName); - - // // setcustomPanelData((prevCustomPanelData) => { - // // const newCustomPanelData = [...prevCustomPanelData]; - // // const renamedCustomPanel = newCustomPanelData.find( - // // (customPanel) => customPanel.id === editedCustomPanelId - // // ); - // // if (renamedCustomPanel) renamedCustomPanel.name = editedCustomPanelName; - // // return newCustomPanelData; - // // }); - // // setToast(`Observability Dashboard successfully renamed into "${editedCustomPanelName}"`); - // } catch (err) { - // console.log( - // 'Error renaming Observability Dashboard, please make sure you have the correct permission.', - // 'danger' - // ); - // console.error(err.body.message); - // } + dispatch(updatePanel(updatedPanel, `Operational Panel successfully renamed into "${editedCustomPanelName}"`, 'Error renaming Operational Panel, please make sure you have the correct permission.')) }; /* diff --git a/public/framework/core_refs.ts b/public/framework/core_refs.ts index e9a3e0e60..dd2367f19 100644 --- a/public/framework/core_refs.ts +++ b/public/framework/core_refs.ts @@ -9,7 +9,7 @@ * GitHub history for details. */ -import { HttpStart } from '../../../../src/core/public'; +import { HttpStart, IToasts } from '../../../../src/core/public'; import { SavedObjectsClientContract } from '../../../../src/core/public'; import PPLService from '../services/requests/ppl'; @@ -19,6 +19,7 @@ class CoreRefs { public http?: HttpStart; public savedObjectsClient?: SavedObjectsClientContract; public pplService?: PPLService; + public toasts?: IToasts; private constructor() { // ... } diff --git a/public/plugin.ts b/public/plugin.ts index bb21d79ae..0a94c6273 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -34,6 +34,7 @@ import { observabilityLogsID, observabilityLogsTitle, observabilityLogsPluginOrder, + observabilityPluginOrder, } from '../common/constants/shared'; import { QueryManager } from '../common/query_manager'; import { VISUALIZATION_SAVED_OBJECT } from '../common/types/observability_saved_object_attributes'; @@ -45,16 +46,9 @@ import { } from '../common/utils'; import { convertLegacyNotebooksUrl } from './components/notebooks/components/helpers/legacy_route_helpers'; import { convertLegacyTraceAnalyticsUrl } from './components/trace_analytics/components/common/legacy_route_helpers'; -// import { uiSettingsService } from '../common/utils'; -// import { QueryManager } from '../common/query_manager'; -import { DashboardSetup } from '../../../src/plugins/dashboard/public'; import { SavedObject } from '../../../src/core/public'; import { coreRefs } from './framework/core_refs'; -// export class ObservabilityPlugin implements Plugin { -// constructor(private initializerContext: PluginInitializerContext) {} - -// public setup(core: CoreSetup, { dashboard }: { dashboard: DashboardSetup }): {} { import { OBSERVABILITY_EMBEDDABLE, OBSERVABILITY_EMBEDDABLE_DESCRIPTION, @@ -68,7 +62,6 @@ import DSLService from './services/requests/dsl'; import PPLService from './services/requests/ppl'; import SavedObjects from './services/saved_objects/event_analytics/saved_objects'; import TimestampUtils from './services/timestamp/timestamp'; -import { observabilityID } from '../common/constants/shared'; import { AppPluginStartDependencies, ObservabilitySetup, @@ -102,10 +95,6 @@ export class ObservabilityPlugin window.location.assign(convertLegacyTraceAnalyticsUrl(window.location)); } - // // redirect legacy notebooks URL to current URL under observability - // if (window.location.pathname.includes('application_analytics')) { - // window.location.assign(convertLegacyAppAnalyticsUrl(window.location)); - // } const BASE_URL = core.http.basePath.prepend('/app/observability-dashboards#'); setupDeps.dashboard.registerDashboardProvider({ appId: 'observability-panel', @@ -124,7 +113,7 @@ export class ObservabilityPlugin label: i18n.translate('core.ui.observabilityNavList.label', { defaultMessage: 'Observability', }), - order: 1500, + order: observabilityPluginOrder, }, }); @@ -240,6 +229,7 @@ export class ObservabilityPlugin coreRefs.http = core.http; coreRefs.savedObjectsClient = core.savedObjects.client; coreRefs.pplService = pplService; + coreRefs.toasts = core.notifications.toasts; return {}; } diff --git a/test/setup.jest.ts b/test/setup.jest.ts index 6bbbefb80..3a6397fe4 100644 --- a/test/setup.jest.ts +++ b/test/setup.jest.ts @@ -60,3 +60,4 @@ setOSDHttp(coreStartMock.http); setOSDSavedObjectsClient(coreStartMock.savedObjects.client); coreRefs.http = coreStartMock.http; coreRefs.savedObjectsClient = coreStartMock.savedObjects.client; +coreRefs.toasts = coreStartMock.notifications.toasts;