diff --git a/.cypress/integration/3_panels.spec.ts b/.cypress/integration/3_panels.spec.ts index 87a4ed902b..251442e393 100644 --- a/.cypress/integration/3_panels.spec.ts +++ b/.cypress/integration/3_panels.spec.ts @@ -74,7 +74,7 @@ describe('Creating visualizations', () => { }); }); -describe('Testing panels table', () => { +describe.only('Testing panels table', () => { beforeEach(() => { eraseTestPanels(); moveToPanelHome(); @@ -597,6 +597,7 @@ const eraseLegacyPanels = () => { 'osd-xsrf': true, }, }).then((response) => { + console.log("legacy panels to erase", response.body) response.body.panels.map((panel) => { cy.request({ method: 'DELETE', @@ -626,6 +627,7 @@ const eraseSavedObjectPaenls = () => { }, }) .then((response) => { + console.log("saved objects to erase", response.body) response.body.saved_objects.map((soPanel) => { cy.request({ method: 'DELETE', @@ -635,6 +637,9 @@ const eraseSavedObjectPaenls = () => { 'content-type': 'application/json;charset=UTF-8', 'osd-xsrf': true, }, + }).then((response) => { + const deletedId = response; + console.log('erased SO Panel', response) }); }); }); @@ -711,9 +716,10 @@ const openActionsDropdown = () => { }; const selectThePanel = () => { - cy.get('.euiCheckbox__input[title="Select this row"]').then(() => { - cy.get('.euiCheckbox__input[title="Select this row"]').check({ force: true }); - }); + // cy.get('.euiCheckbox__input[title="Select this row"]').then(() => { + cy.get('.euiCheckbox__input[title="Select this row"]').check({ force: true }); + cy.get('.euiTableRow-isSelected').should('exist') + // }); }; const expectToastWith = (title) => { diff --git a/common/types/shared.ts b/common/types/shared.ts new file mode 100644 index 0000000000..fd0f0a71e1 --- /dev/null +++ b/common/types/shared.ts @@ -0,0 +1,5 @@ +import { CoreStart, ToastsStart } from '../../../../src/core/public'; + +export interface ObservabilityAppServices extends CoreStart { + toasts: ToastsStart; +} diff --git a/public/components/app.tsx b/public/components/app.tsx index 18066f3281..8ea7e3834b 100644 --- a/public/components/app.tsx +++ b/public/components/app.tsx @@ -18,6 +18,8 @@ import { EventAnalytics } from './event_analytics'; import { Home as MetricsHome } from './metrics/index'; import { Main as NotebooksHome } from './notebooks/components/main'; import { Home as TraceAnalyticsHome } from './trace_analytics/home'; +import { ObservabilityAppServices } from '../../common/types/shared'; +import { OpenSearchDashboardsContextProvider } from '../../../../src/plugins/opensearch_dashboards_react/public'; interface ObservabilityAppDeps { CoreStartProp: CoreStart; @@ -28,6 +30,7 @@ interface ObservabilityAppDeps { timestampUtils: any; queryManager: QueryManager; startPage: string; + services: ObservabilityAppServices; } // for cypress to test redux store @@ -53,6 +56,7 @@ export const App = ({ timestampUtils, queryManager, startPage, + services, }: ObservabilityAppDeps) => { const { chrome, http, notifications, savedObjects: coreSavedObjects } = CoreStartProp; const parentBreadcrumb = { @@ -65,26 +69,28 @@ export const App = ({ return ( - - - + + + + + ); diff --git a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap index 05379e0bcd..226fddb62b 100644 --- a/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap +++ b/public/components/custom_panels/__tests__/__snapshots__/custom_panel_view.test.tsx.snap @@ -2,707 +2,589 @@ exports[`Panels View Component renders panel view container with visualizations 1`] = ` - + - - - + + - - - - - - - + + + + - - - - - + + + - - - - - Created on - Invalid date - - - - - + + + + + Created on + Invalid date + + + + - - - - + + - - - - - - - - - - - - - Edit + + + + + + + Edit + - - - - - - - - - + + + + + + - - Dashboard Actions - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - isOpen={false} - ownFocus={true} - panelPaddingSize="none" - withTitle={true} - > - - - - - - - - - - - - - - - - Dashboard Actions - - - - - - - - - - - - - - - Add visualization + Dashboard Actions } closePopover={[Function]} display="inlineBlock" hasArrow={true} - id="addVisualizationContextMenu" isOpen={false} ownFocus={true} panelPaddingSize="none" + withTitle={true} > - Add visualization + Dashboard Actions @@ -788,115 +670,206 @@ exports[`Panels View Component renders panel view container with visualizations - - - - - - - - - - - - + + + + + + Add visualization + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="addVisualizationContextMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + + + + + + + + + + + + + + Add visualization + + + + + + + + + + + + + + + + + + + + - - - - + + + + PPL + + } + baseQuery="source = " + dslService={ + DSLService { + "fetch": [Function], + "fetchFields": [Function], + "fetchIndices": [Function], + "http": [MockFunction], } - > - PPL - - } - baseQuery="source = " - dslService={ - DSLService { - "fetch": [Function], - "fetchFields": [Function], - "fetchIndices": [Function], - "http": [MockFunction], } - } - getSuggestions={[Function]} - handleQueryChange={[Function]} - handleQuerySearch={[Function]} - isDisabled={true} - key="autocomplete-search-bar" - onItemSelect={[Function]} - placeholder="Use PPL 'where' clauses to add filters on all visualizations [where Carrier = 'OpenSearch-Air']" - possibleCommands={ - Array [ - Object { - "label": "where", - }, - ] - } - query="" - tabId="panels-filter" - tempQuery="" - > - - - PPL - - } - aria-autocomplete="both" + - } + aria-autocomplete="both" + aria-labelledby="autocomplete-4-label" + autoCapitalize="off" + autoComplete="off" + autoCorrect="off" + autoFocus={false} + data-test-subj="searchAutocompleteTextArea" + disabled={true} + enterKeyHint="search" fullWidth={true} - inputId="autocomplete-textarea" + id="autocomplete-textarea" + maxLength={512} + onBlur={[Function]} + onChange={[Function]} + onClick={[Function]} + onFocus={[Function]} + onKeyDown={[Function]} + placeholder="Use PPL 'where' clauses to add filters on all visualizations [where Carrier = 'OpenSearch-Air']" + spellCheck="false" + type="search" + value="" > - + PPL + + } + fullWidth={true} + inputId="autocomplete-textarea" > - - - - - - - + + + + + + - PPL - - - - - - - - - - - + PPL + + + + + + + + + + - - - - - - - } + + + - + } > - - - - + - - + - + + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + isOpen={false} + ownFocus={true} + panelPaddingSize="m" + > + + + - + - - - - - - - - - - + + + + + + + + + + - - - - + + + + - - - - - } - iconType={false} - isCustom={true} - startDateControl={} + + + - } + iconType={false} + isCustom={true} + startDateControl={} > - - Last 30 minutes - - - Show dates - - - - - - - - - - - - - + Show dates + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + - Refresh - + + Refresh + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - - - - Start by adding your first visualization - - - - - - + Start by adding your first visualization + + - + + + - - Use PPL Queries to fetch & filter observability data and create visualizations - - - - - - - - - - - - - + Use PPL Queries to fetch & filter observability data and create visualizations + + + + + + + + + - + + + - - - - Add visualization - - } - closePopover={[Function]} - display="inlineBlock" - hasArrow={true} - id="addVisualizationContextMenu" - isOpen={false} - ownFocus={true} - panelPaddingSize="none" + - - + - + } + closePopover={[Function]} + display="inlineBlock" + hasArrow={true} + id="addVisualizationContextMenu" + isOpen={false} + ownFocus={true} + panelPaddingSize="none" + > + + + - - - - - - - - - - - - Add visualization + + + + + + + Add visualization + - - - - - + + + + + - - - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - - + maxRows={Infinity} + onDrag={[Function]} + onDragStart={[Function]} + onDragStop={[Function]} + onLayoutChange={[Function]} + onResize={[Function]} + onResizeStart={[Function]} + onResizeStop={[Function]} + preventCollision={false} + rowHeight={150} + style={Object {}} + useCSSTransforms={true} + verticalCompact={true} + width={0} + > + + + + + + + + + + + + + + `; 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 d4eb276e66..5a5da64e15 100644 --- a/public/components/custom_panels/__tests__/custom_panel_view.test.tsx +++ b/public/components/custom_panels/__tests__/custom_panel_view.test.tsx @@ -24,6 +24,7 @@ import { applyMiddleware, createStore } from 'redux'; import { rootReducer } from '../../../framework/redux/reducers'; import thunk from 'redux-thunk'; import { Provider } from 'react-redux'; +import { OpenSearchDashboardsContextProvider } from '../../../../../../src/plugins/opensearch_dashboards_react/public'; describe('Panels View Component', () => { configure({ adapter: new Adapter() }); @@ -73,6 +74,26 @@ describe('Panels View Component', () => { page="operationalPanels" /> + + + ); wrapper.update(); @@ -113,27 +134,31 @@ describe('Panels View Component', () => { window.location.assign(`#/event_analytics/explorer/${savedVisId}`); }; + const services = { toasts: { addDanger: (t) => {} } } + const wrapper = mount( - - - + + + + + ); wrapper.update(); diff --git a/public/components/custom_panels/custom_panel_table.tsx b/public/components/custom_panels/custom_panel_table.tsx index 4b1fd571d5..b0e6f2c206 100644 --- a/public/components/custom_panels/custom_panel_table.tsx +++ b/public/components/custom_panels/custom_panel_table.tsx @@ -50,6 +50,7 @@ import { DeleteModal } from '../common/helpers/delete_modal'; import { createPanel, deletePanels, + doesNameExist, fetchPanels, isUuid, newPanelTemplate, @@ -120,7 +121,10 @@ export const CustomPanelTable = ({ }; const onCreate = async (newCustomPanelName: string) => { - if (!isNameValid(newCustomPanelName)) { + const nameFlag = await doesNameExist(newCustomPanelName); + if (await nameFlag()) { + setToast(`Observability Dashboard with name "${newCustomPanelName}" already exists`, 'danger'); + } else if (!isNameValid(newCustomPanelName)) { setToast('Invalid Dashboard name', 'danger'); } else { const newPanel = newPanelTemplate(newCustomPanelName); @@ -130,7 +134,10 @@ export const CustomPanelTable = ({ }; const onRename = async (newCustomPanelName: string) => { - if (!isNameValid(newCustomPanelName)) { + const nameFlag = await doesNameExist(newCustomPanelName); + if (await nameFlag()) { + setToast(`Observability Dashboard with name "${newCustomPanelName}" already exists`, 'danger'); + } else if (!isNameValid(newCustomPanelName)) { setToast('Invalid Dashboard name', 'danger'); } else { dispatch(renameCustomPanel(newCustomPanelName, selectedCustomPanels[0].id)); @@ -139,7 +146,10 @@ export const CustomPanelTable = ({ }; const onClone = async (newName: string) => { - if (!isNameValid(newName)) { + const nameFlag = await doesNameExist(newName); + if (await nameFlag()) { + setToast(`Observability Dashboard with name "${newName}" already exists`, 'danger'); + } else if (!isNameValid(newName)) { setToast('Invalid Operational Panel name', 'danger'); } else { let sourcePanel = selectedCustomPanels[0]; @@ -219,7 +229,7 @@ export const CustomPanelTable = ({ 'Duplicate Dashboard', 'Cancel', 'Duplicate', - selectedCustomPanels[0].title + ' (copy)', + selectedCustomPanels[0].title + ' (copy)x', CREATE_PANEL_MESSAGE ) ); @@ -227,9 +237,8 @@ export const CustomPanelTable = ({ }; const deletePanel = () => { - const customPanelString = `Observability Dashboard${ - selectedCustomPanels.length > 1 ? 's' : '' - }`; + const customPanelString = `Observability Dashboard${selectedCustomPanels.length > 1 ? 's' : '' + }`; setModalLayout( - customPanel.title.toLowerCase().includes(searchQuery.toLowerCase()) - ) + customPanel.title.toLowerCase().includes(searchQuery.toLowerCase()) + ) : customPanels } itemId="id" diff --git a/public/components/custom_panels/custom_panel_view_so.tsx b/public/components/custom_panels/custom_panel_view_so.tsx index 65b10416a0..4aadb9109c 100644 --- a/public/components/custom_panels/custom_panel_view_so.tsx +++ b/public/components/custom_panels/custom_panel_view_so.tsx @@ -57,6 +57,7 @@ import { VisaulizationFlyoutSO } from './panel_modules/visualization_flyout/visu import { clonePanel, deletePanels, + doesNameExist, fetchPanel, renameCustomPanel, selectPanel, @@ -125,7 +126,6 @@ export const CustomPanelViewSO = (props: CustomPanelViewProps) => { } = props; const dispatch = useDispatch(); - const { setToast } = useToast(); const panel = useSelector(selectPanel); const [loading, setLoading] = useState(true); @@ -148,6 +148,8 @@ export const CustomPanelViewSO = (props: CustomPanelViewProps) => { const [editActionType, setEditActionType] = useState(''); const [isHelpFlyoutVisible, setHelpIsFlyoutVisible] = useState(false); + const { setToast } = useToast(); + const appPanel = page === 'app'; const closeHelpFlyout = () => { diff --git a/public/components/custom_panels/redux/panel_slice.ts b/public/components/custom_panels/redux/panel_slice.ts index 1bad8f4523..b35ece78d1 100644 --- a/public/components/custom_panels/redux/panel_slice.ts +++ b/public/components/custom_panels/redux/panel_slice.ts @@ -141,6 +141,14 @@ 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 doesNameExist = (newCustomPanelName: string) => async() => { + const panels = await fetchCustomPanels(); + if((panels.some((i: { title: string; }) => i.title === newCustomPanelName))){ + return true; + } + return false; +} + export const updatePanel = (panel: CustomPanelType, successMsg: string, failureMsg: string) => async (dispatch, getState) => { try { if (isUuid(panel.id)) await updateSavedObjectPanel(panel); @@ -247,7 +255,7 @@ export const createPanel = (panel) => async (dispatch, getState) => { ); console.error(e); } -}; +} export const createPanelSample = (vizIds) => async (dispatch, getState) => { const samplePanel = { diff --git a/public/components/index.tsx b/public/components/index.tsx index 8191752974..440f2518a2 100644 --- a/public/components/index.tsx +++ b/public/components/index.tsx @@ -9,6 +9,7 @@ import { QueryManager } from 'common/query_manager'; import { AppMountParameters, CoreStart } from '../../../../src/core/public'; import { AppPluginStartDependencies } from '../types'; import { App } from './app'; +import { ObservabilityAppServices } from '../../common/types/shared'; export const Observability = ( CoreStartProp: CoreStart, @@ -19,6 +20,7 @@ export const Observability = ( savedObjects: any, timestampUtils: any, queryManager: QueryManager, + services: ObservabilityAppServices, startPage: string ) => { ReactDOM.render( @@ -31,6 +33,7 @@ export const Observability = ( timestampUtils={timestampUtils} queryManager={queryManager} startPage={startPage} + services={services} />, AppMountParametersProp.element ); diff --git a/public/framework/redux/reducers/toast_slice.ts b/public/framework/redux/reducers/toast_slice.ts new file mode 100644 index 0000000000..de856535f7 --- /dev/null +++ b/public/framework/redux/reducers/toast_slice.ts @@ -0,0 +1,40 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const toastSlice = createSlice({ + name: 'toast', + initialState: { toasts: [], toastRightSide: true }, + reducers: { + appendToast: (state, action) => { + state.toasts = [...state.toasts, action.payload]; + }, + + setToastRightSide: (state, action) => (state.toastRightSide = action.payload), + + dismissToast: (state, action) => { + state.toasts = state.toasts.filter((t) => t.id !== action.payload.id); + }, + + resetToasts: (state, action) => { + state.toasts = []; + state.toastRightSide = true; + }, + }, +}); + +export const toastReducer = toastSlice.reducer; + +export const { dismissToast, resetToasts } = toastSlice.actions; +const { appendToast, setToastRightSide } = toastSlice.actions; + +export const selectToasts = (rootState) => rootState.toast.toasts; + +export const selectToastRightSide = (rootState) => rootState.toast.toastRightSide; + +export const addToast = (title, color?, textChild?, side?) => (dispatch, getState) => { + const newToast = { id: new Date().toISOString(), title, textChild, color }; + dispatch(appendToast(newToast)); + + if (side) { + dispatch(setToastRightSide(side === 'left')); + } +}; diff --git a/public/plugin.ts b/public/plugin.ts index 0a94c6273c..215226bd5e 100644 --- a/public/plugin.ts +++ b/public/plugin.ts @@ -68,10 +68,11 @@ import { ObservabilityStart, SetupDependencies, } from './types'; +import { ObservabilityAppServices } from '../common/types/shared'; export class ObservabilityPlugin implements - Plugin { + Plugin { public setup( core: CoreSetup, setupDeps: SetupDependencies @@ -124,6 +125,10 @@ export class ObservabilityPlugin const savedObjects = new SavedObjects(coreStart.http); const timestampUtils = new TimestampUtils(dslService, pplService); + const services: ObservabilityAppServices = { + toasts: coreStart.notifications.toasts, + }; + return Observability( coreStart, depsStart as AppPluginStartDependencies, @@ -133,6 +138,7 @@ export class ObservabilityPlugin savedObjects, timestampUtils, qm, + services, startPage ); };