diff --git a/common/types/custom_panels.ts b/common/types/custom_panels.ts index ea7d03cf4..4d8f62145 100644 --- a/common/types/custom_panels.ts +++ b/common/types/custom_panels.ts @@ -9,6 +9,13 @@ * GitHub history for details. */ +export type CustomPanelListType = { + name: string; + id: string; + dateCreated: string; + dateModified: string; +}; + export type VisualizationType = { id: string; title: string; diff --git a/public/components/custom_panels/custom_panel_table.tsx b/public/components/custom_panels/custom_panel_table.tsx index 1124358c2..8c33c5005 100644 --- a/public/components/custom_panels/custom_panel_table.tsx +++ b/public/components/custom_panels/custom_panel_table.tsx @@ -54,29 +54,30 @@ const pageStyles: CSSProperties = { /* * "CustomPanelTable" module, used to view all the saved panels + * + * Props taken in as params are: * loading: loader bool for the table * fetchCustomPanels: fetch panels function * customPanels: List of panels available * createCustomPanel: create panel function * setBreadcrumbs: setter for breadcrumbs on top panel * parentBreadcrumb: parent breadcrumb - * renameCustomPanel: delete function for the panel + * renameCustomPanel: rename function for the panel * cloneCustomPanel: clone function for the panel - * deleteCustomPanel: delete function for the panel + * deleteCustomPanelList: delete function for the panels * setToast: create Toast function */ type Props = { loading: boolean; - fetchCustomPanels: () => void; + fetchCustomPanels: () => Promise; customPanels: Array; createCustomPanel: (newCustomPanelName: string) => void; setBreadcrumbs: (newBreadcrumbs: ChromeBreadcrumb[]) => void; parentBreadcrumb: EuiBreadcrumb[]; renameCustomPanel: (newCustomPanelName: string, customPanelId: string) => void; cloneCustomPanel: (newCustomPanelName: string, customPanelId: string) => void; - deleteCustomPanel: (customPanelId: string, customPanelName?: string, showToast?: boolean) => void; - setToast: (title: string, color?: string, text?: string) => void; + deleteCustomPanelList: (customPanelIdList: string[], toastMessage: string) => any; }; export const CustomPanelTable = ({ @@ -88,8 +89,7 @@ export const CustomPanelTable = ({ parentBreadcrumb, renameCustomPanel, cloneCustomPanel, - deleteCustomPanel, - setToast, + deleteCustomPanelList, }: Props) => { const [isModalVisible, setIsModalVisible] = useState(false); // Modal Toggle const [modalLayout, setModalLayout] = useState(); // Modal Layout @@ -129,17 +129,8 @@ export const CustomPanelTable = ({ const toastMessage = `Custom Panels ${ selectedCustomPanels.length > 1 ? 's' : ' ' + selectedCustomPanels[0].name } successfully deleted!`; - Promise.all( - selectedCustomPanels.map((customPanel) => deleteCustomPanel(customPanel.id, undefined, false)) - ) - .then(() => setToast(toastMessage)) - .catch((err) => { - setToast( - 'Error deleting Operational Panels, please make sure you have the correct permission.', - 'danger' - ); - console.error(err.body.message); - }); + const PanelList = selectedCustomPanels.map((panel) => panel.id); + deleteCustomPanelList(PanelList, toastMessage); closeModal(); }; @@ -263,13 +254,13 @@ export const CustomPanelTable = ({ field: 'dateModified', name: 'Last updated', sortable: true, - render: (value) => moment(value).format(UI_DATE_FORMAT), + render: (value) => moment(new Date(value)).format(UI_DATE_FORMAT), }, { field: 'dateCreated', name: 'Created', sortable: true, - render: (value) => moment(value).format(UI_DATE_FORMAT), + render: (value) => moment(new Date(value)).format(UI_DATE_FORMAT), }, ] as Array< EuiTableFieldDataColumnType<{ @@ -296,7 +287,8 @@ export const CustomPanelTable = ({

- Panels ({customPanels.length}) + Panels + ({customPanels.length})

diff --git a/public/components/custom_panels/custom_panel_view.tsx b/public/components/custom_panels/custom_panel_view.tsx index a369ea0e0..d5373dc63 100644 --- a/public/components/custom_panels/custom_panel_view.tsx +++ b/public/components/custom_panels/custom_panel_view.tsx @@ -12,6 +12,7 @@ import { EuiBreadcrumb, EuiButton, + EuiContextMenu, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -21,6 +22,7 @@ import { EuiPageContentBody, EuiPageHeader, EuiPageHeaderSection, + EuiPopover, EuiSpacer, EuiSuperDatePicker, EuiSuperDatePickerProps, @@ -32,28 +34,26 @@ import _ from 'lodash'; import React, { useEffect, useState } from 'react'; import { CoreStart } from '../../../../../src/core/public'; import { EmptyPanelView } from './panel_modules/empty_panel'; -import { AddVizView } from './panel_modules/add_visualization'; import { RENAME_VISUALIZATION_MESSAGE, CREATE_PANEL_MESSAGE, CUSTOM_PANELS_API_PREFIX, - VisualizationType, } from '../../../common/constants/custom_panels'; +import { VisualizationType } from '../../../common/types/custom_panels'; import { PanelGrid } from './panel_modules/panel_grid'; -import { - DeletePanelModal, - DeleteVisualizationModal, - getCustomModal, -} from './helpers/modal_containers'; +import { DeletePanelModal, getCustomModal } from './helpers/modal_containers'; import PPLService from '../../services/requests/ppl'; -import { convertDateTime, getNewVizDimensions, onTimeChange } from './helpers/utils'; +import { isDateValid, convertDateTime, onTimeChange, isPPLFilterValid } from './helpers/utils'; import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; import { UI_DATE_FORMAT } from '../../../common/constants/shared'; import { ChangeEvent } from 'react'; import moment from 'moment'; +import { VisaulizationFlyout } from './panel_modules/visualization_flyout'; /* * "CustomPanelsView" module used to render an Operational Panel + * + * Props taken in as params are: * panelId: Name of the panel opened * http: http core service * pplService: ppl requestor service @@ -70,9 +70,17 @@ type Props = { pplService: PPLService; chrome: CoreStart['chrome']; parentBreadcrumb: EuiBreadcrumb[]; - renameCustomPanel: (newCustomPanelName: string, customPanelId: string) => void; - deleteCustomPanel: (customPanelId: string, customPanelName?: string, showToast?: boolean) => void; - setToast: (title: string, color?: string, text?: string) => void; + renameCustomPanel: ( + editedCustomPanelName: string, + editedCustomPanelId: string + ) => Promise | undefined; + deleteCustomPanel: (customPanelId: string, customPanelName: string) => Promise; + setToast: ( + title: string, + color?: string, + text?: React.ReactChild | undefined, + side?: string | undefined + ) => void; }; export const CustomPanelView = ({ @@ -93,34 +101,76 @@ export const CustomPanelView = ({ const [inputDisabled, setInputDisabled] = useState(true); const [addVizDisabled, setAddVizDisabled] = useState(false); const [editDisabled, setEditDisabled] = useState(false); - const [showVizPanel, setShowVizPanel] = useState(false); const [panelVisualizations, setPanelVisualizations] = useState>([]); const [editMode, setEditMode] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); // Modal Toggle const [modalLayout, setModalLayout] = useState(); // Modal Layout + const [isVizPopoverOpen, setVizPopoverOpen] = useState(false); // Add Visualization Popover + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); // Add Visualization Flyout + const [isFlyoutReplacement, setisFlyoutReplacement] = useState(false); + const [replaceVisualizationId, setReplaceVisualizationId] = useState(''); // DateTimePicker States const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([]); const [start, setStart] = useState('now-30m'); const [end, setEnd] = useState('now'); + const getVizContextPanels = (closeVizPopover?: () => void) => { + return [ + { + id: 0, + title: 'Add Visualization', + items: [ + { + name: 'Select Existing Visualization', + onClick: () => { + if (closeVizPopover != null) { + closeVizPopover(); + } + showFlyout(); + }, + }, + { + name: 'Create New Visualization', + onClick: () => { + advancedVisualization(); + }, + }, + ], + }, + ]; + }; + + const advancedVisualization = () => { + closeVizPopover(); + window.location.assign('#/event_analytics/explorer'); + }; + // Fetch Panel by id const fetchCustomPanel = async () => { return http .get(`${CUSTOM_PANELS_API_PREFIX}/panels/${panelId}`) .then((res) => { - setOpenPanelName(res.panel.name); - setPanelCreatedTime(res.panel.dateCreated); - setPanelVisualizations(res.panel.visualizations); - setPPLFilterValue(_.unescape(res.panel.queryFilter.query)); - setStart(res.panel.timeRange.from); - setEnd(res.panel.timeRange.to); + setOpenPanelName(res.operationalPanel.name); + setPanelCreatedTime(res.createdTimeMs); + setPPLFilterValue(res.operationalPanel.queryFilter.query); + setStart(res.operationalPanel.timeRange.from); + setEnd(res.operationalPanel.timeRange.to); + setPanelVisualizations(res.operationalPanel.visualizations); }) .catch((err) => { console.error('Issue in fetching the operational panels', err); }); }; + const onPopoverClick = () => { + setVizPopoverOpen(!isVizPopoverOpen); + }; + + const closeVizPopover = () => { + setVizPopoverOpen(false); + }; + const onChange = (e: ChangeEvent) => { setPPLFilterValue(e.target.value); }; @@ -134,8 +184,9 @@ export const CustomPanelView = ({ }; const onDelete = async () => { - const toastMessage = `Operational Panel ${openPanelName} successfully deleted!`; - deleteCustomPanel(panelId, openPanelName); + deleteCustomPanel(panelId, openPanelName).then((res) => { + window.location.assign(`${_.last(parentBreadcrumb).href}`); + }); closeModal(); }; @@ -152,7 +203,9 @@ export const CustomPanelView = ({ }; const onRename = async (newCustomPanelName: string) => { - renameCustomPanel(newCustomPanelName, panelId); + renameCustomPanel(newCustomPanelName, panelId).then(() => { + setOpenPanelName(newCustomPanelName); + }); closeModal(); }; @@ -174,18 +227,24 @@ export const CustomPanelView = ({ // toggle between panel edit mode const editPanel = () => { - setEditMode(!editMode); - setShowVizPanel(false); + if (editMode) { + // Save layout + setEditMode(false); + } else { + setEditMode(true); + } }; - const closeVizWindow = () => { - setShowVizPanel(false); + const closeFlyout = () => { + setIsFlyoutVisible(false); setAddVizDisabled(false); checkDisabledInputs(); }; - const addVizWindow = () => { - setShowVizPanel(true); + const showFlyout = (isReplacement?: boolean, replaceVizId?: string) => { + setisFlyoutReplacement(isReplacement); + setReplaceVisualizationId(replaceVizId); + setIsFlyoutVisible(true); setAddVizDisabled(true); setInputDisabled(true); }; @@ -204,13 +263,40 @@ export const CustomPanelView = ({ }; const onRefreshFilters = () => { - setOnRefresh(!onRefresh); + if (!isDateValid(convertDateTime(start), convertDateTime(end, false), setToast)) { + return; + } + + if (!isPPLFilterValid(pplFilterValue, setToast)) { + return; + } + + const panelFilterBody = { + panelId: panelId, + query: pplFilterValue, + language: 'ppl', + to: end, + from: start, + }; + + http + .patch(`${CUSTOM_PANELS_API_PREFIX}/panels/filter`, { + body: JSON.stringify(panelFilterBody), + }) + .then((res) => { + setOnRefresh(!onRefresh); + }) + .catch((err) => { + setToast('Error is adding filters to the operational panel', 'danger'); + console.error(err.body.message); + }); }; const cloneVisualization = ( newVisualizationTitle: string, pplQuery: string, - newVisualizationType: string + newVisualizationType: string, + newVisualizationTimeField: string ) => { setModalLayout( getCustomModal( @@ -222,8 +308,7 @@ export const CustomPanelView = ({ 'Duplicate', newVisualizationTitle + ' (copy)', RENAME_VISUALIZATION_MESSAGE, - pplQuery, - newVisualizationType + [pplQuery, newVisualizationType, newVisualizationTimeField] ) ); showModal(); @@ -232,46 +317,86 @@ export const CustomPanelView = ({ const onCloneVisualization = ( newVisualizationTitle: string, pplQuery: string, - newVisualizationType: string + newVisualizationType: string, + newVisualizationTimeField: string ) => { - const newDimensions = getNewVizDimensions(panelVisualizations); - setPanelVisualizations([ - ...panelVisualizations, - { - id: htmlIdGenerator()(), - title: newVisualizationTitle, - query: pplQuery, - type: newVisualizationType, - ...newDimensions, - }, - ]); - - //NOTE: Make a backend call to Clone Visualization + http + .post(`${CUSTOM_PANELS_API_PREFIX}/visualizations`, { + body: JSON.stringify({ + panelId: panelId, + newVisualization: { + id: 'panelViz_' + htmlIdGenerator()(), + title: newVisualizationTitle, + query: pplQuery, + type: newVisualizationType, + timeField: newVisualizationTimeField, + }, + }), + }) + .then(async (res) => { + setPanelVisualizations(res.visualizations); + setToast(`Visualization ${newVisualizationTitle} successfully added!`, 'success'); + }) + .catch((err) => { + setToast(`Error in adding ${newVisualizationTitle} visualization to the panel`, 'danger'); + console.error(err); + }); closeModal(); }; - const deleteVisualization = (visualizationId: string, visualizationName: string) => { - setModalLayout( - - ); - showModal(); + const removeVisualization = (visualizationId: string) => { + const newVisualizationList = _.reject(panelVisualizations, { + id: visualizationId, + }); + if (newVisualizationList.length === 0) { + setEditMode(false); + http + .put(`${CUSTOM_PANELS_API_PREFIX}/visualizations/edit`, { + body: JSON.stringify({ + panelId: panelId, + visualizationParams: [], + }), + }) + .then(async (res) => { + setPanelVisualizations(res.visualizations); + }) + .catch((err) => { + console.error(err); + }); + } + setPanelVisualizations(newVisualizationList); }; - const onDeleteVisualization = (visualizationId: string) => { - const filteredPanelVisualizations = panelVisualizations.filter( - (panelVisualization) => panelVisualization.id != visualizationId - ); - setPanelVisualizations([...filteredPanelVisualizations]); + //Add Visualization Button + const addVisualizationButton = ( + + Add Visualization + + ); - //NOTE: Make a backend call to Delete Visualization - closeModal(); - }; + let flyout; + if (isFlyoutVisible) { + flyout = ( + + ); + } // Fetch the custom panel on Initial Mount useEffect(() => { @@ -280,10 +405,7 @@ export const CustomPanelView = ({ // Check Validity of Time useEffect(() => { - if (convertDateTime(end, false) < convertDateTime(start)) { - setToast('Invalid Time Interval', 'danger'); - return; - } + isDateValid(convertDateTime(start), convertDateTime(end, false), setToast); }, [start, end]); // Toggle input type (disabled or not disabled) @@ -296,7 +418,10 @@ export const CustomPanelView = ({ useEffect(() => { chrome.setBreadcrumbs([ ...parentBreadcrumb, - { text: openPanelName, href: `${_.last(parentBreadcrumb).href}${panelId}` }, + { + text: openPanelName, + href: `${_.last(parentBreadcrumb).href}${panelId}`, + }, ]); }, [openPanelName]); @@ -324,9 +449,6 @@ export const CustomPanelView = ({ Rename - - Export - onChange(e)} @@ -374,46 +496,52 @@ export const CustomPanelView = ({ Refresh - - - Add visualization - + + + + - {panelVisualizations.length == 0 ? ( - !showVizPanel && ( - - ) + {panelVisualizations.length === 0 ? ( + ) : ( )} - <> - {showVizPanel && ( - - )} - + <> {isModalVisible && modalLayout} + {flyout} ); }; diff --git a/public/components/custom_panels/forms/add_new_visualizations.tsx b/public/components/custom_panels/forms/add_new_visualizations.tsx deleted file mode 100644 index f060c415d..000000000 --- a/public/components/custom_panels/forms/add_new_visualizations.tsx +++ /dev/null @@ -1,276 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -import { - EuiSpacer, - EuiFormRow, - EuiFieldText, - EuiLink, - EuiTextArea, - EuiButtonEmpty, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiButtonIcon, - EuiIcon, - EuiText, - EuiSuperDatePicker, - ShortDate, - EuiSuperDatePickerProps, -} from '@elastic/eui'; -import { htmlIdGenerator } from '@elastic/eui/lib/services'; -import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; -import { UI_DATE_FORMAT } from '..././../../common/constants/shared'; -import _ from 'lodash'; -import React, { Fragment, useEffect, useState } from 'react'; -import { VisualizationType } from '../../../../common/constants/custom_panels'; -import PPLService from '../../../services/requests/ppl'; -import { Plt } from '../../visualizations/plotly/plot'; -import { - convertDateTime, - getNewVizDimensions, - getQueryResponse, - isNameValid, - onTimeChange, -} from '../helpers/utils'; - -/* - * "AddNewVisualizations" component to add new visualizations using PPL Queries - * - * closeVizWindow: function to close "add visualization" window - * pplService: PPLService Requestor - * panelVisualizations: panelVisualizations object - * setPanelVisualizations: Setter for panelVisualizations object - * setToast: Create Toast function - */ - -type Props = { - closeVizWindow: () => void; - pplService: PPLService; - panelVisualizations: VisualizationType[]; - setPanelVisualizations: React.Dispatch>; - setToast: (title: string, color?: string, text?: string) => void; -}; - -export const AddNewVisualizations = ({ - closeVizWindow, - pplService, - panelVisualizations, - setPanelVisualizations, - setToast, -}: Props) => { - const [newVisualizationTitle, setNewVisualizationTitle] = useState(''); - const [newVisualizationType, setNewVisualizationType] = useState('bar'); - const [pplQuery, setPPLQuery] = useState(''); - const [previewData, setPreviewData] = useState([]); - const [previewArea, setPreviewArea] = useState(<>); - const [showPreviewArea, setShowPreviewArea] = useState(false); - const [previewIconType, setPreviewIconType] = useState('arrowRight'); - const [previewLoading, setPreviewLoading] = useState(false); - const [isPreviewError, setIsPreviewError] = useState(''); - - // DateTimePicker States - const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([]); - const [start, setStart] = useState('now-30m'); - const [end, setEnd] = useState('now'); - - const onPreviewClick = () => { - if (previewIconType == 'arrowRight') { - setPreviewIconType('arrowUp'); - setShowPreviewArea(true); - } else { - setPreviewIconType('arrowRight'); - setShowPreviewArea(false); - } - }; - - const onRefreshPreview = () => { - if (convertDateTime(end, false) < convertDateTime(start)) { - setToast('Invalid Time Interval', 'danger'); - return; - } - - getQueryResponse( - pplService, - pplQuery, - 'bar', - start, - end, - setPreviewData, - setPreviewLoading, - setIsPreviewError, - '' - ); - }; - - useEffect(() => { - const previewTemplate = - isPreviewError == '' ? ( - <> - - - - - - - ) : ( - <> -
- - - - -

Error in rendering the visualizaiton

-
- - -

{isPreviewError}

-
-
- - ); - - setPreviewArea(previewTemplate); - }, [previewLoading]); - - const onChangeText = ( - e: React.ChangeEvent | React.ChangeEvent, - setter: React.Dispatch> - ) => { - setter(e.target.value); - }; - - const advancedVisualization = () => { - //NOTE: Add Redux functions to pass pplquery and time filters to events page - window.location.assign('#/explorer/events'); - }; - - const addVisualization = () => { - if (!isNameValid(newVisualizationTitle)) { - setToast('Invalid Visualization Name', 'danger'); - return; - } - - if (pplQuery.length == 0) { - setToast('Invalid PPL Query', 'danger'); - return; - } - - const newDimensions = getNewVizDimensions(panelVisualizations); - setPanelVisualizations([ - ...panelVisualizations, - { - id: htmlIdGenerator()(), - title: newVisualizationTitle, - query: _.escape(pplQuery), - type: newVisualizationType, - ...newDimensions, - }, - ]); - - //NOTE: Add a backend call to add a visualization - setToast(`Visualization ${newVisualizationTitle} successfully added!`, 'success'); - closeVizWindow(); - }; - - return ( - <> - - - onChangeText(e, setNewVisualizationTitle)} - /> - - - Use [example commands] to draw visaulizations.{' '} - - Learn More - {' '} - - } - fullWidth={true} - > - onChangeText(e, setPPLQuery)} - fullWidth={true} - style={{ width: '80%' }} - /> - - - ) => - onTimeChange( - props.start, - props.end, - recentlyUsedRanges, - setRecentlyUsedRanges, - setStart, - setEnd - ) - } - showUpdateButton={false} - recentlyUsedRanges={recentlyUsedRanges} - /> - - - - - - Preview - - - - - - - - {showPreviewArea && previewArea} - - More advanced edit options in visual editor... - - - - - - Add - - - - Cancel - - - - ); -}; diff --git a/public/components/custom_panels/forms/add_saved_visualizations.tsx b/public/components/custom_panels/forms/add_saved_visualizations.tsx deleted file mode 100644 index 9d208347c..000000000 --- a/public/components/custom_panels/forms/add_saved_visualizations.tsx +++ /dev/null @@ -1,135 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -import { - EuiSpacer, - EuiFormRow, - EuiSelect, - EuiSuperDatePicker, - EuiButtonEmpty, - EuiButton, - EuiFlexGroup, - EuiFlexItem, - ShortDate, - EuiSuperDatePickerProps, -} from '@elastic/eui'; -import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; -import { UI_DATE_FORMAT } from '..././../../common/constants/shared'; -import React, { useState } from 'react'; -import { VisualizationType } from '../../../../common/constants/custom_panels'; -import PPLService from '../../../services/requests/ppl'; -import { onTimeChange } from '../helpers/utils'; - -//NOTE: working this module is TBD after work on storing ppl visualizations in index is complete - -/* - * "AddSavedVisualizations" component to add saved visualizations - * - * closeVizWindow: function to close "add visualization" window - * pplService: PPLService Requestor - * panelVisualizations: panelVisualizations object - * setPanelVisualizations: Setter for panelVisualizations object - * setToast: Create Toast function - */ - -type Props = { - closeVizWindow: () => void; - pplService: PPLService; - panelVisualizations: VisualizationType[]; - setPanelVisualizations: React.Dispatch>; - setToast: (title: string, color?: string, text?: string) => void; -}; - -export const AddSavedVisualizations = ({ - closeVizWindow, - pplService, - panelVisualizations, - setPanelVisualizations, - setToast, -}: Props) => { - const [newVisualizationTitle, setNewVisualizationTitle] = useState(''); - const [newVisualizationType, setNewVisualizationType] = useState('bar'); - const [pplQuery, setPPLQuery] = useState(''); - const [previewData, setPreviewData] = useState([]); - const [previewArea, setPreviewArea] = useState(<>); - const [showPreviewArea, setShowPreviewArea] = useState(false); - const [previewIconType, setPreviewIconType] = useState('arrowRight'); - const [previewLoading, setPreviewLoading] = useState(false); - const [isPreviewError, setIsPreviewError] = useState(''); - - // DateTimePicker States - const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([]); - const [start, setStart] = useState('now-30m'); - const [end, setEnd] = useState('now'); - - const onPreviewClick = () => { - if (previewIconType == 'arrowRight') { - setPreviewIconType('arrowUp'); - setShowPreviewArea(true); - } else { - setPreviewIconType('arrowRight'); - setShowPreviewArea(false); - } - }; - - return ( - <> - - - {}} - options={[ - { value: 'option_one', text: 'Option one' }, - { value: 'option_two', text: 'Option two' }, - { value: 'option_three', text: 'Option three' }, - ]} - /> - - - ) => - onTimeChange( - props.start, - props.end, - recentlyUsedRanges, - setRecentlyUsedRanges, - setStart, - setEnd - ) - } - showUpdateButton={false} - recentlyUsedRanges={recentlyUsedRanges} - /> - - - - Preview - - - {showPreviewArea && previewArea} - - - - - Add - - - - Cancel - - - - ); -}; diff --git a/public/components/custom_panels/helpers/custom_input_modal.tsx b/public/components/custom_panels/helpers/custom_input_modal.tsx index d9c1bcd23..5dbc40070 100644 --- a/public/components/custom_panels/helpers/custom_input_modal.tsx +++ b/public/components/custom_panels/helpers/custom_input_modal.tsx @@ -35,10 +35,14 @@ import { * btn1txt - string as content to fill "close button" * btn2txt - string as content to fill "confirm button" * openPanelName - Default input value for the field + * helpText - string help for the input field + * optionalArgs - Arguments needed to pass them to runModal function */ type CustomInputModalProps = { - runModal: (value: string, value2?: string, value3?: string) => void; + runModal: + | ((value: string, value2: string, value3: string, value4: string) => void) + | ((value: string) => void); closeModal: ( event?: React.KeyboardEvent | React.MouseEvent ) => void; @@ -48,8 +52,7 @@ type CustomInputModalProps = { btn2txt: string; openPanelName?: string; helpText?: string; - optionalArg1?: string; - optionalArg2?: string; + optionalArgs?: string[]; }; export const CustomInputModal = (props: CustomInputModalProps) => { @@ -62,8 +65,7 @@ export const CustomInputModal = (props: CustomInputModalProps) => { btn2txt, openPanelName, helpText, - optionalArg1, - optionalArg2, + optionalArgs, } = props; const [value, setValue] = useState(openPanelName || ''); // sets input value @@ -88,12 +90,12 @@ export const CustomInputModal = (props: CustomInputModalProps) => { {btn1txt} - {optionalArg1 === undefined ? ( + {optionalArgs === undefined ? ( runModal(value)} fill> {btn2txt} ) : ( - runModal(value, optionalArg1, optionalArg2)} fill> + runModal(value, ...optionalArgs)} fill> {btn2txt} )} diff --git a/public/components/custom_panels/helpers/flyout_containers.tsx b/public/components/custom_panels/helpers/flyout_containers.tsx new file mode 100644 index 000000000..ed23fddf5 --- /dev/null +++ b/public/components/custom_panels/helpers/flyout_containers.tsx @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { EuiFlyout } from '@elastic/eui'; +import React from 'react'; + +/* + * "FlyoutContainers" component used to create flyouts + * + * Props taken in as params are: + * flyoutHeader - header JSX element of flyout + * flyoutBody - body JSX element of flyout + * flyoutFooter - footer JSX element of flyout + * ariaLabel - aria-label for focus of flyout + */ + +type Props = { + closeFlyout: () => void; + flyoutHeader: JSX.Element; + flyoutBody: JSX.Element; + flyoutFooter: JSX.Element; + ariaLabel: string; +}; + +export const FlyoutContainers = ({ + closeFlyout, + flyoutHeader, + flyoutBody, + flyoutFooter, + ariaLabel, +}: Props) => { + return ( +
+ closeFlyout()} aria-labelledby={ariaLabel}> + {flyoutHeader} + {flyoutBody} + {flyoutFooter} + +
+ ); +}; diff --git a/public/components/custom_panels/helpers/modal_containers.tsx b/public/components/custom_panels/helpers/modal_containers.tsx index 5a0b3b478..136c32135 100644 --- a/public/components/custom_panels/helpers/modal_containers.tsx +++ b/public/components/custom_panels/helpers/modal_containers.tsx @@ -35,7 +35,9 @@ import { CustomInputModal } from './custom_input_modal'; */ export const getCustomModal = ( - runModal: ((value: string, value2: string, value3: string) => void) | ((value: string) => void), + runModal: + | ((value: string, value2: string, value3: string, value4: string) => void) + | ((value: string) => void), closeModal: ( event?: React.KeyboardEvent | React.MouseEvent ) => void, @@ -45,8 +47,7 @@ export const getCustomModal = ( btn2txt: string, openPanelName?: string, helpText?: string, - optionalArg1?: string, - optionalArg2?: string + optionalArgs?: string[] ) => { return ( ); }; @@ -162,41 +162,3 @@ export const DeletePanelModal = ({ ); }; - -export const DeleteVisualizationModal = ({ - onConfirm, - onCancel, - visualizationId, - visualizationName, - panelName, -}: { - onConfirm: (value: string) => void; - onCancel: ( - event?: - | React.KeyboardEvent - | React.MouseEvent - | undefined - ) => void; - visualizationId: string; - visualizationName: string; - panelName: string; -}) => { - return ( - - onConfirm(visualizationId)} - cancelButtonText="Cancel" - confirmButtonText="Remove" - buttonColor="danger" - defaultFocusedButton="confirm" - > -

- Remove “{visualizationName}” from “{panelName}”? You can still retrieve the visualization - by name. -

-
-
- ); -}; diff --git a/public/components/custom_panels/helpers/utils.tsx b/public/components/custom_panels/helpers/utils.tsx index 74a5577a3..5ebbcfbc3 100644 --- a/public/components/custom_panels/helpers/utils.tsx +++ b/public/components/custom_panels/helpers/utils.tsx @@ -13,12 +13,23 @@ import dateMath from '@elastic/datemath'; import { ShortDate } from '@elastic/eui'; import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; import _ from 'lodash'; -import { VisualizationType } from '../../../../common/constants/custom_panels'; +import { Moment } from 'moment-timezone'; import { PPL_DATE_FORMAT, PPL_INDEX_REGEX } from '../../../../common/constants/shared'; import PPLService from '../../../services/requests/ppl'; +import React from 'react'; +import { Bar } from '../../visualizations/charts/bar'; +import { HorizontalBar } from '../../visualizations/charts/horizontal_bar'; +import { Line } from '../../visualizations/charts/line'; /* * "Utils" This file contains different reused functions in operational panels + * isNameValid - Validates string to length > 0 and < 50 + * convertDateTime - Converts input datetime string to required format + * getQueryResponse - Get response of PPL query to load visualizations + * onTimeChange - Function to store recently used time filters and set start and end time. + * isDateValid - Function to check date validity + * isPPLFilterValid - Validate if the panel PPL query doesn't contain any Index/Time/Field filters + * displayVisualization - This function renders the visualzation based of its type */ // Name validation 0>Name<=50 @@ -27,18 +38,26 @@ export const isNameValid = (name: string) => { }; // DateTime convertor to required format -export const convertDateTime = (datetime: string, isStart = true) => { - if (isStart) return dateMath.parse(datetime).format(PPL_DATE_FORMAT); - return dateMath.parse(datetime, { roundUp: true }).format(PPL_DATE_FORMAT); +export const convertDateTime = (datetime: string, isStart = true, formatted = true) => { + let returnTime: undefined | Moment; + if (isStart) { + returnTime = dateMath.parse(datetime); + } else { + returnTime = dateMath.parse(datetime, { roundUp: true }); + } + + if (formatted) return returnTime.format(PPL_DATE_FORMAT); + return returnTime; }; -// Builds Final Query by adding time and query filters to the original visualization query -// -> Final Query is as follows: -// -> finalQuery = indexPartOfQuery + timeQueryFilter + panelFilterQuery + filterPartOfQuery -// -> finalQuery = source=opensearch_dashboards_sample_data_flights -// + | where utc_time > timestamp(‘2021-07-01 00:00:00’) and utc_time < timestamp(‘2021-07-02 00:00:00’) -// + | where Carrier='OpenSearch-Air' -// + | stats sum(FlightDelayMin) as delays by Carrier +/* Builds Final Query by adding time and query filters(From panel UI) to the original visualization query + * -> Final Query is as follows: + * -> finalQuery = indexPartOfQuery + timeQueryFilter + panelFilterQuery + filterPartOfQuery + * -> finalQuery = source=opensearch_dashboards_sample_data_flights + * + | where utc_time > timestamp(‘2021-07-01 00:00:00’) and utc_time < timestamp(‘2021-07-02 00:00:00’) + * + | where Carrier='OpenSearch-Air' + * + | stats sum(FlightDelayMin) as delays by Carrier + */ const queryAccumulator = ( originalQuery: string, timestampField: string, @@ -59,8 +78,32 @@ const queryAccumulator = ( return indexPartOfQuery + timeQueryFilter + pplFilterQuery + filterPartOfQuery; }; +//PPL Service requestor +const pplServiceRequestor = async ( + pplService: PPLService, + finalQuery: string, + type: string, + setVisualizationData: React.Dispatch>, + setIsLoading: React.Dispatch>, + setIsError: React.Dispatch> +) => { + await pplService + .fetch({ query: finalQuery, format: 'viz' }) + .then((res) => { + if (res === undefined) setIsError('Please check the PPL Filter Value'); + setVisualizationData(res); + }) + .catch((error: Error) => { + setIsError(error.stack); + console.error(error); + }) + .finally(() => { + setIsLoading(false); + }); +}; + // Get PPL Query Response -export const getQueryResponse = async ( +export const getQueryResponse = ( pplService: PPLService, query: string, type: string, @@ -77,53 +120,14 @@ export const getQueryResponse = async ( let finalQuery = ''; try { - finalQuery = queryAccumulator( - _.unescape(query), - timestampField, - startTime, - endTime, - filterQuery - ); + finalQuery = queryAccumulator(query, timestampField, startTime, endTime, filterQuery); } catch (error) { - console.log('Issue in building final query', error.stack); + console.error('Issue in building final query', error.stack); setIsLoading(false); return; } - await pplService - .fetch({ query: finalQuery, format: 'viz' }) - .then((res) => { - setVisualizationData([ - { - x: res.data[res.metadata.xfield.name], - y: res.data[res.metadata.yfield.name], - type: type, - }, - ]); - }) - .catch((error: Error) => { - setIsError(error.stack); - console.error(err); - }) - .finally(() => { - setIsLoading(false); - }); -}; - -// Calculate new visualization dimensions -// New visualization always joins to the end of the panel -export const getNewVizDimensions = (panelVisualizations: VisualizationType[]) => { - let maxY: number = 0, - maxYH: number = 0; - - panelVisualizations.map((panelVisualization: VisualizationType) => { - if (maxY < panelVisualization.y) { - maxY = panelVisualization.y; - maxYH = panelVisualization.h; - } - }); - - return { x: 0, y: maxY + maxYH, w: 6, h: 4 }; + pplServiceRequestor(pplService, finalQuery, type, setVisualizationData, setIsLoading, setIsError); }; // Function to store recently used time filters and set start and end time. @@ -142,7 +146,99 @@ export const onTimeChange = ( recentlyUsedRange.unshift({ start, end }); setStart(start); setEnd(end); - setRecentlyUsedRanges( - recentlyUsedRange.length > 10 ? recentlyUsedRange.slice(0, 9) : recentlyUsedRange - ); + setRecentlyUsedRanges(recentlyUsedRange.slice(0, 9)); +}; + +// Function to check date validity +export const isDateValid = ( + start: string | Moment | undefined, + end: string | Moment | undefined, + setToast: ( + title: string, + color?: string, + text?: React.ReactChild | undefined, + side?: string | undefined + ) => void, + side?: string | undefined +) => { + if (end! < start!) { + setToast('Time range entered is invalid', 'danger', undefined, side); + return false; + } else return true; +}; + +// Check for time filter in query +const checkIndexExists = (query: string) => { + return PPL_INDEX_REGEX.test(query); +}; + +// Check PPL Query in Panel UI +// Validate if the query doesn't contain any Index +export const isPPLFilterValid = ( + query: string, + setToast: ( + title: string, + color?: string, + text?: React.ReactChild | undefined, + side?: string | undefined + ) => void +) => { + if (checkIndexExists(query)) { + setToast('Please remove index from PPL Filter', 'danger', undefined); + return false; + } + return true; +}; + +// This function renders the visualzation based of its type +export const displayVisualization = (data: any, type: string) => { + if (data === undefined) return; + + let vizComponent!: JSX.Element; + switch (type) { + case 'bar': { + vizComponent = ( + + ); + break; + } + case 'horizontal_bar': { + vizComponent = ( + + ); + break; + } + case 'line': { + vizComponent = ( + + ); + break; + } + default: { + vizComponent = <>; + break; + } + } + return vizComponent; }; diff --git a/public/components/custom_panels/home.tsx b/public/components/custom_panels/home.tsx index 1ffe5b33f..4742b0cd6 100644 --- a/public/components/custom_panels/home.tsx +++ b/public/components/custom_panels/home.tsx @@ -11,10 +11,11 @@ import { EuiBreadcrumb, EuiGlobalToastList, EuiLink } from '@elastic/eui'; import { Toast } from '@elastic/eui/src/components/toast/global_toast_list'; +import { CustomPanelListType } from '../../../common/types/custom_panels'; import _ from 'lodash'; import React, { ReactChild, useState } from 'react'; import { StaticContext } from 'react-router'; -import { Route, RouteComponentProps, RouteProps } from 'react-router-dom'; +import { Route, RouteComponentProps } from 'react-router-dom'; import { CoreStart } from '../../../../../src/core/public'; import { CUSTOM_PANELS_API_PREFIX, @@ -27,6 +28,8 @@ import { isNameValid } from './helpers/utils'; /* * "Home" module is initial page for Operantional Panels + * + * Props taken in as params are: * http: http core service; * chrome: chrome core service; * parentBreadcrumb: parent breadcrumb name and link @@ -42,20 +45,15 @@ type Props = { renderProps: RouteComponentProps; }; -export type CustomPanelListType = { - name: string; - id: string; - dateCreated: string; - dateModified: string; -}; - export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps }: Props) => { const [customPanelData, setcustomPanelData] = useState>([]); const [toasts, setToasts] = useState>([]); const [loading, setLoading] = useState(false); + const [toastRightSide, setToastRightSide] = useState(true); - const setToast = (title: string, color = 'success', text?: ReactChild) => { + 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]); }; @@ -63,7 +61,9 @@ export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps } const fetchCustomPanels = () => { return http .get(`${CUSTOM_PANELS_API_PREFIX}/panels`) - .then((res) => setcustomPanelData(res.panels)) + .then((res) => { + setcustomPanelData(res.panels); + }) .catch((err) => { console.error('Issue in fetching the operational panels', err.body.message); }); @@ -77,14 +77,14 @@ export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps } } return http - .post(`${CUSTOM_PANELS_API_PREFIX}/panel`, { + .post(`${CUSTOM_PANELS_API_PREFIX}/panels`, { body: JSON.stringify({ - name: newCustomPanelName, + panelName: newCustomPanelName, }), }) .then(async (res) => { setToast(`Operational Panel "${newCustomPanelName}" successfully created!`); - window.location.assign(`${_.last(parentBreadcrumb).href}${res}`); + window.location.assign(`${_.last(parentBreadcrumb).href}${res.newPanelId}`); }) .catch((err) => { setToast( @@ -99,25 +99,25 @@ export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps } }; // Renames an existing CustomPanel - const renameCustomPanel = (editedCustomPanelName: string, editedCustomPanelID: string) => { + const renameCustomPanel = (editedCustomPanelName: string, editedCustomPanelId: string) => { if (!isNameValid(editedCustomPanelName)) { setToast('Invalid Custom Panel name', 'danger'); return; } const renamePanelObject = { - name: editedCustomPanelName, - panelId: editedCustomPanelID, + panelId: editedCustomPanelId, + panelName: editedCustomPanelName, }; return http - .put(`${CUSTOM_PANELS_API_PREFIX}/panel/rename`, { + .patch(`${CUSTOM_PANELS_API_PREFIX}/panels/rename`, { body: JSON.stringify(renamePanelObject), }) .then((res) => { setcustomPanelData((prevCustomPanelData) => { const newCustomPanelData = [...prevCustomPanelData]; const renamedCustomPanel = newCustomPanelData.find( - (customPanel) => customPanel.id === editedCustomPanelID + (customPanel) => customPanel.id === editedCustomPanelId ); if (renamedCustomPanel) renamedCustomPanel.name = editedCustomPanelName; return newCustomPanelData; @@ -134,21 +134,18 @@ export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps } }; // Clones an existing Custom Panel, return new Custom Panel id - const cloneCustomPanel = ( - clonedCustomPanelName: string, - clonedCustomPanelID: string - ): Promise => { + const cloneCustomPanel = (clonedCustomPanelName: string, clonedCustomPanelId: string) => { if (!isNameValid(clonedCustomPanelName)) { setToast('Invalid Operational Panel name', 'danger'); return Promise.reject(); } const clonePanelObject = { - name: clonedCustomPanelName, - panelId: clonedCustomPanelID, + panelId: clonedCustomPanelId, + panelName: clonedCustomPanelName, }; return http - .post(`${CUSTOM_PANELS_API_PREFIX}/panel/clone`, { + .post(`${CUSTOM_PANELS_API_PREFIX}/panels/clone`, { body: JSON.stringify(clonePanelObject), }) .then((res) => { @@ -157,14 +154,13 @@ export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps } ...prevCustomPanelData, { name: clonedCustomPanelName, - id: res.body.id, - dateCreated: res.body.dateCreated, - dateModified: res.body.dateModified, + id: res.clonePanelId, + dateCreated: res.dateCreated, + dateModified: res.dateModified, }, ]; }); setToast(`Operational Panel "${clonedCustomPanelName}" successfully created!`); - return res.body.id; }) .catch((err) => { setToast( @@ -176,14 +172,37 @@ export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps } }; // Deletes an existing Operational Panel - const deleteCustomPanel = (customPanelId: string, customPanelName?: string, showToast = true) => { + const deleteCustomPanelList = (customPanelIdList: string[], toastMessage: string) => { + const concatList = customPanelIdList.toString(); return http - .delete(`${CUSTOM_PANELS_API_PREFIX}/panel/` + customPanelId) + .delete(`${CUSTOM_PANELS_API_PREFIX}/panelList/` + concatList) + .then((res) => { + setcustomPanelData((prevCustomPanelData) => { + return prevCustomPanelData.filter( + (customPanel) => !customPanelIdList.includes(customPanel.id) + ); + }); + setToast(toastMessage); + return res; + }) + .catch((err) => { + setToast( + 'Error deleting Operational Panels, please make sure you have the correct permission.', + 'danger' + ); + console.error(err.body.message); + }); + }; + + // Deletes an existing Operational Panel + const deleteCustomPanel = (customPanelId: string, customPanelName: string) => { + return http + .delete(`${CUSTOM_PANELS_API_PREFIX}/panels/` + customPanelId) .then((res) => { setcustomPanelData((prevCustomPanelData) => { return prevCustomPanelData.filter((customPanel) => customPanel.id !== customPanelId); }); - if (showToast) setToast(`Operational Panel "${customPanelName}" successfully deleted!`); + setToast(`Operational Panel "${customPanelName}" successfully deleted!`); return res; }) .catch((err) => { @@ -202,6 +221,7 @@ export const Home = ({ http, chrome, parentBreadcrumb, pplService, renderProps } dismissToast={(removedToast) => { setToasts(toasts.filter((toast) => toast.id !== removedToast.id)); }} + side={toastRightSide ? 'right' : 'left'} toastLifeTimeMs={6000} /> ); }} diff --git a/public/components/custom_panels/panel_modules/add_visualization.tsx b/public/components/custom_panels/panel_modules/add_visualization.tsx deleted file mode 100644 index 52469e10e..000000000 --- a/public/components/custom_panels/panel_modules/add_visualization.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiForm, - EuiText, - EuiHorizontalRule, - EuiCheckableCard, -} from '@elastic/eui'; -import React, { useState } from 'react'; -import PPLService from '../../../services/requests/ppl'; -import { VisualizationType } from '../../../../common/constants/custom_panels'; -import { htmlIdGenerator } from '@elastic/eui/lib/services'; -import { AddSavedVisualizations } from '../forms/add_saved_visualizations'; -import { AddNewVisualizations } from '../forms/add_new_visualizations'; - -/* - * "AddNewVisualizations" component to add new visualizations using PPL Queries - * - * closeVizWindow: function to close "add visualization" window - * pplService: PPLService Requestor - * panelVisualizations: panelVisualizations object - * setPanelVisualizations: Setter for panelVisualizations object - * setToast: Create Toast function - */ - -type Props = { - closeVizWindow: () => void; - pplService: PPLService; - panelVisualizations: VisualizationType[]; - setPanelVisualizations: React.Dispatch>; - setToast: (title: string, color?: string, text?: string) => void; -}; - -export const AddVizView = ({ - closeVizWindow, - pplService, - panelVisualizations, - setPanelVisualizations, - setToast, -}: Props) => { - const radioName = htmlIdGenerator()(); - const [radio, setRadio] = useState('radio1'); - - return ( -
- - -

Add visualization

-
- -
- - - setRadio('radio1')} - /> - - - setRadio('radio2')} - /> - - - {radio === 'radio1' ? ( - - ) : ( - - )} -
-
-
- ); -}; diff --git a/public/components/custom_panels/panel_modules/empty_panel.tsx b/public/components/custom_panels/panel_modules/empty_panel.tsx index e6dfd6116..7b1785d2d 100644 --- a/public/components/custom_panels/panel_modules/empty_panel.tsx +++ b/public/components/custom_panels/panel_modules/empty_panel.tsx @@ -9,22 +9,62 @@ * GitHub history for details. */ -import { EuiSpacer, EuiText, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import React from 'react'; +import { + EuiSpacer, + EuiText, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiPopover, + EuiContextMenu, +} from '@elastic/eui'; +import React, { useState } from 'react'; /* -* EmptyPanelView -* This Sub-component is shown to the user when a operational panel is empty -* Props: -* addVizWindow -> This function shows the add visualization window in operational panels view -*/ + * EmptyPanelView - This Sub-component is shown to the user when a operational panel is empty + * + * Props taken in as params are: + * addVizDisabled -> Boolean to enable/disable the add visualization button + * getVizContextPanels -> Function to populate the add visualization popover + */ type Props = { - addVizWindow: () => void; addVizDisabled: boolean; + getVizContextPanels: ( + closeVizPopover?: (() => void) | undefined + ) => { + id: number; + title: string; + items: { + name: string; + onClick: () => void; + }[]; + }[]; }; -export const EmptyPanelView = ({ addVizWindow, addVizDisabled }: Props) => { +export const EmptyPanelView = ({ addVizDisabled, getVizContextPanels }: Props) => { + const [isVizPopoverOpen, setVizPopoverOpen] = useState(false); + + const onPopoverClick = () => { + setVizPopoverOpen(!isVizPopoverOpen); + }; + + const closeVizPopover = () => { + setVizPopoverOpen(false); + }; + + //Add Visualization Button + const addVisualizationButton = ( + + Add Visualization + + ); + return (
@@ -38,9 +78,16 @@ export const EmptyPanelView = ({ addVizWindow, addVizDisabled }: Props) => { - - Add Visualization - + + + diff --git a/public/components/custom_panels/panel_modules/panel_grid/index.ts b/public/components/custom_panels/panel_modules/panel_grid/index.ts new file mode 100644 index 000000000..15e88cc23 --- /dev/null +++ b/public/components/custom_panels/panel_modules/panel_grid/index.ts @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export { PanelGrid } from "./panel_grid"; diff --git a/public/components/custom_panels/panel_modules/panel_grid/panel_grid.scss b/public/components/custom_panels/panel_modules/panel_grid/panel_grid.scss new file mode 100644 index 000000000..1b36abe7a --- /dev/null +++ b/public/components/custom_panels/panel_modules/panel_grid/panel_grid.scss @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +.react-grid-layout { + & .react-grid-placeholder { + background: $euiColorWarning; + } +} + +.full-width { + min-width: 100%; + max-width: 100%; +} diff --git a/public/components/custom_panels/panel_modules/panel_grid.tsx b/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx similarity index 55% rename from public/components/custom_panels/panel_modules/panel_grid.tsx rename to public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx index 0e50d67a5..039f9f4e3 100644 --- a/public/components/custom_panels/panel_modules/panel_grid.tsx +++ b/public/components/custom_panels/panel_modules/panel_grid/panel_grid.tsx @@ -13,49 +13,72 @@ import _ from 'lodash'; import React, { useEffect, useState } from 'react'; import { Layout, Layouts, Responsive, WidthProvider } from 'react-grid-layout'; import useObservable from 'react-use/lib/useObservable'; -import { CoreStart } from '../../../../../../src/core/public'; -import PPLService from '../../../services/requests/ppl'; -import { VisualizationContainer } from './visualization_container'; -import { VisualizationType } from '../../../../common/constants/custom_panels'; +import { CoreStart } from '../../../../../../../src/core/public'; +import PPLService from '../../../../services/requests/ppl'; +import { VisualizationContainer } from '../visualiation_container'; +import { VisualizationType } from '../../../../../common/types/custom_panels'; +import { CUSTOM_PANELS_API_PREFIX } from '../../../../../common/constants/custom_panels'; +import './panel_grid.scss'; // HOC container to provide dynamic width for Grid layout const ResponsiveGridLayout = WidthProvider(Responsive); /* -* PanelGrid - This module is places all visualizations in react-grid-layout -* chrome: CoreStart['chrome']; -* panelVisualizations: VisualizationType[]; -* editMode: boolean; -* pplService: PPLService; -* startTime: string; -* endTime: string; -* onRefresh: boolean; -*/ + * PanelGrid - This module is places all visualizations in react-grid-layout + * + * Props taken in as params are: + * http: http core service; + * chrome: chrome core service; + * panelVisualizations: list of panel visualizations + * setPanelVisualizations: function to set panel visualizations + * editMode: boolean to check if the panel is in edit mode + * pplService: ppl requestor service + * startTime: start time in date filter + * endTime: end time in date filter + * onRefresh: boolean value to trigger refresh of visualizations + * cloneVisualization: function to clone a visualization in panel + * pplFilterValue: string with panel PPL filter value + * showFlyout: function to show the flyout + * removeVisualization: function to remove all the visualizations + */ type Props = { + http: CoreStart['http']; chrome: CoreStart['chrome']; + panelId: string; panelVisualizations: VisualizationType[]; + setPanelVisualizations: React.Dispatch>; editMode: boolean; pplService: PPLService; startTime: string; endTime: string; onRefresh: boolean; - cloneVisualization: (newVisualizationTitle: string, pplQuery: string, newVisualizationType: string) => void; - deleteVisualization: (visualizationId: string, visualizationName: string)=> void; + cloneVisualization: ( + newVisualizationTitle: string, + pplQuery: string, + newVisualizationType: string, + newVisualizationTimeField: string + ) => void; pplFilterValue: string; + showFlyout: (isReplacement?: boolean | undefined, replaceVizId?: string | undefined) => void; + removeVisualization: (visualizationId: string) => void; }; export const PanelGrid = ({ + http, chrome, + panelId, panelVisualizations, + setPanelVisualizations, editMode, pplService, startTime, endTime, onRefresh, cloneVisualization, - deleteVisualization, pplFilterValue, + showFlyout, + removeVisualization, }: Props) => { const [layout, setLayout] = useState([]); const [editedLayout, setEditedLayout] = useState([]); @@ -69,7 +92,7 @@ export const PanelGrid = ({ // Reload the Layout const reloadLayout = () => { - const tempLayout:Layout[] = panelVisualizations.map((panelVisualization) => { + const tempLayout: Layout[] = panelVisualizations.map((panelVisualization) => { return { i: panelVisualization.id, x: panelVisualization.x, @@ -82,6 +105,22 @@ export const PanelGrid = ({ setLayout(tempLayout); }; + const saveVisualizationLayouts = async (panelId: string, visualizationParams: any) => { + return http + .put(`${CUSTOM_PANELS_API_PREFIX}/visualizations/edit`, { + body: JSON.stringify({ + panelId: panelId, + visualizationParams: visualizationParams, + }), + }) + .then(async (res) => { + setPanelVisualizations(res.visualizations); + }) + .catch((err) => { + console.error(err); + }); + }; + // Update layout whenever user edit gets completed useEffect(() => { if (editMode) { @@ -90,8 +129,9 @@ export const PanelGrid = ({ const newLayout = editedLayout.map((element) => { return { ...element, static: true }; }); + const visualizationParams = newLayout.map((layout) => _.omit(layout, ['static', 'moved'])); setLayout(newLayout); - // NOTE: need to add backend call to change visualization sizes + if (visualizationParams.length !== 0) saveVisualizationLayouts(panelId, visualizationParams); } }, [editMode]); @@ -110,13 +150,12 @@ export const PanelGrid = ({ return ( - {panelVisualizations.map((panelVisualization: VisualizationType, index: number) => ( + {panelVisualizations.map((panelVisualization: VisualizationType) => (
))} diff --git a/public/components/custom_panels/panel_modules/visualiation_container/index.ts b/public/components/custom_panels/panel_modules/visualiation_container/index.ts new file mode 100644 index 000000000..ae7fc7cec --- /dev/null +++ b/public/components/custom_panels/panel_modules/visualiation_container/index.ts @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export { VisualizationContainer } from "./visualization_container"; diff --git a/public/components/custom_panels/panel_modules/visualiation_container/visualization_container.scss b/public/components/custom_panels/panel_modules/visualiation_container/visualization_container.scss new file mode 100644 index 000000000..79e517fc2 --- /dev/null +++ b/public/components/custom_panels/panel_modules/visualiation_container/visualization_container.scss @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + + .mouseGrabber { + & :hover { + cursor: -webkit-grab; + cursor: grab; + } + & :active { + cursor: -webkit-grabbing; + cursor: grabbing; + } +} + +.visualization-div { + width: 100%; + height: 90%; + overflow: scroll; + text-align: center; +} + +%center-div { + top: 50%; + left: 50%; + -ms-transform: translate(-50%, -50%); + -moz-transform: translate(-50%, -50%); + -webkit-transform: translate(-50%, -50%); + -o-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); +} + +.visualization-loading-chart { + margin: 0; + position: absolute; + @extend %center-div; +} + +.visualization-error-div { + overflow: scroll; + position: relative; + @extend %center-div; +} + +.panel-full-width { + width: 100%; + height: 100%; +} diff --git a/public/components/custom_panels/panel_modules/visualiation_container/visualization_container.tsx b/public/components/custom_panels/panel_modules/visualiation_container/visualization_container.tsx new file mode 100644 index 000000000..bb035c50e --- /dev/null +++ b/public/components/custom_panels/panel_modules/visualiation_container/visualization_container.tsx @@ -0,0 +1,206 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLoadingChart, + EuiPanel, + EuiPopover, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import React, { useEffect, useMemo, useState } from 'react'; +import PPLService from '../../../../services/requests/ppl'; +import { displayVisualization, getQueryResponse } from '../../helpers/utils'; +import './visualization_container.scss'; + +/* + * Visualization container - This module is a placeholder to add visualizations in react-grid-layout + * + * Props taken in as params are: + * editMode: boolean to check if the panel is in edit mode + * visualizationId: unique visualization id + * visualizationTitle: visualization name + * query: ppl query to load the visualization + * pplService: ppl requestor service + * type: type of visualization [bar, horizontal_bar, line] + * fromTime: start time in date filter + * toTime: end time in date filter + * onRefresh: boolean value to trigger refresh of visualizations + * cloneVisualization: function to clone a visualization in panel + * pplFilterValue: string with panel PPL filter value + * showFlyout: function to show the flyout + * removeVisualization: function to remove all the visualizations + */ + +type Props = { + editMode: boolean; + visualizationId: string; + visualizationTitle: string; + query: string; + pplService: PPLService; + type: string; + timeField: string; + fromTime: string; + toTime: string; + onRefresh: boolean; + cloneVisualization: ( + newVisualizationTitle: string, + pplQuery: string, + newVisualizationType: string, + newVisualizationTimeField: string + ) => void; + pplFilterValue: string; + showFlyout: (isReplacement?: boolean | undefined, replaceVizId?: string | undefined) => void; + removeVisualization: (visualizationId: string) => void; +}; + +export const VisualizationContainer = ({ + editMode, + visualizationId, + visualizationTitle, + pplService, + query, + type, + timeField, + fromTime, + toTime, + onRefresh, + cloneVisualization, + pplFilterValue, + showFlyout, + removeVisualization, +}: Props) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [disablePopover, setDisablePopover] = useState(false); + const [visualizationData, setVisualizationData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isError, setIsError] = useState(''); + const onActionsMenuClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen); + const closeActionsMenu = () => setIsPopoverOpen(false); + + const popoverPanel = [ + { + closeActionsMenu(); + showFlyout(true, visualizationId); + }} + > + Replace + , + { + closeActionsMenu(); + cloneVisualization(visualizationTitle, query, type, timeField); + }} + > + Duplicate + , + ]; + + const loadVisaulization = async () => { + await getQueryResponse( + pplService, + query, + type, + fromTime, + toTime, + setVisualizationData, + setIsLoading, + setIsError, + pplFilterValue, + timeField + ); + }; + + const memoisedVisualizationBox = useMemo( + () => ( +
+ {isLoading ? ( + + ) : isError != '' ? ( +
+ + + + +

Error in rendering the visualizaiton

+
+ + +

{isError}

+
+
+ ) : ( + displayVisualization(visualizationData, type) + )} +
+ ), + [onRefresh, isLoading, isError, visualizationData, type] + ); + + useEffect(() => { + loadVisaulization(); + }, [onRefresh]); + + useEffect(() => { + editMode ? setDisablePopover(true) : setDisablePopover(false); + }, [editMode]); + + return ( + +
+ + + +
{visualizationTitle}
+
+
+ + {disablePopover ? ( + { + removeVisualization(visualizationId); + }} + /> + ) : ( + + } + isOpen={isPopoverOpen} + closePopover={closeActionsMenu} + anchorPosition="downLeft" + > + + + )} + +
+
+ {memoisedVisualizationBox} +
+ ); +}; diff --git a/public/components/custom_panels/panel_modules/visualization_container.tsx b/public/components/custom_panels/panel_modules/visualization_container.tsx deleted file mode 100644 index 2555234a2..000000000 --- a/public/components/custom_panels/panel_modules/visualization_container.tsx +++ /dev/null @@ -1,174 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -import { - EuiButtonIcon, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiLoadingChart, - EuiPanel, - EuiPopover, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import { Plt } from '../../visualizations/plotly/plot'; -import React, { useEffect, useRef, useState } from 'react'; -import PPLService from '../../../services/requests/ppl'; -import { getQueryResponse } from '../helpers/utils'; - -// Visualization Panel module allows view added viz modules. - -type Props = { - editMode: boolean; - visualizationId: string; - visualizationTitle: string; - query: string; - pplService: PPLService; - type: string; - fromTime: string; - toTime: string; - onRefresh: boolean; - cloneVisualization: ( - newVisualizationTitle: string, - pplQuery: string, - newVisualizationType: string - ) => void; - deleteVisualization: (visualizationId: string, visualizationName: string) => void; - pplFilterValue: string; -}; - -export const VisualizationContainer = ({ - editMode, - visualizationId, - visualizationTitle, - pplService, - query, - type, - fromTime, - toTime, - onRefresh, - cloneVisualization, - deleteVisualization, - pplFilterValue, -}: Props) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [disablePopover, setDisablePopover] = useState(false); - const [visualizationData, setVisualizationData] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [isError, setIsError] = useState(''); - const onActionsMenuClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen); - const closeActionsMenu = () => setIsPopoverOpen(false); - - const popoverPanel = [ - - View events - , - - Edit - , - cloneVisualization(visualizationTitle, query, type)} - > - Duplicate - , - deleteVisualization(visualizationId, visualizationTitle)} - > - Remove - , - ]; - - const loadVisaulization = async () => { - await getQueryResponse( - pplService, - query, - type, - fromTime, - toTime, - setVisualizationData, - setIsLoading, - setIsError, - pplFilterValue, - ); - }; - - useEffect(() => { - loadVisaulization(); - }, [query, onRefresh]); - - useEffect(() => { - editMode ? setDisablePopover(true) : setDisablePopover(false); - }, [editMode]); - - return ( - - - - -
{visualizationTitle}
-
-
- - } - isOpen={isPopoverOpen} - closePopover={closeActionsMenu} - anchorPosition="downLeft" - > - - - -
-
- {isLoading ? ( - - ) : isError != '' ? ( -
- - - - -

Error in rendering the visualizaiton

-
- - -

{isError}

-
-
- ) : ( - - )} -
-
- ); -}; diff --git a/public/components/custom_panels/panel_modules/visualization_flyout/index.ts b/public/components/custom_panels/panel_modules/visualization_flyout/index.ts new file mode 100644 index 000000000..980a5dae8 --- /dev/null +++ b/public/components/custom_panels/panel_modules/visualization_flyout/index.ts @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +export { VisaulizationFlyout } from "./visualization_flyout"; diff --git a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.scss b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.scss new file mode 100644 index 000000000..d86d7ca53 --- /dev/null +++ b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.scss @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +.visualization-div { + min-height: 200; +} + +.visualization-loading-chart { + margin: 0; + position: absolute; + top: 50%; + left: 50%; + -ms-transform: translate(-50%, -50%); + -moz-transform: translate(-50%, -50%); + -webkit-transform: translate(-50%, -50%); + -o-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); +} + +.visualization-error-div { + overflow: scroll; +} + +.date-picker-height { + height: 3vh; +} diff --git a/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx new file mode 100644 index 000000000..4b90d439b --- /dev/null +++ b/public/components/custom_panels/panel_modules/visualization_flyout/visualization_flyout.tsx @@ -0,0 +1,399 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +import { + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiCallOut, + EuiDatePicker, + EuiDatePickerRange, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiFormRow, + EuiIcon, + EuiLoadingChart, + EuiSelect, + EuiSelectOption, + EuiSpacer, + EuiText, + EuiTitle, + htmlIdGenerator, + ShortDate, +} from '@elastic/eui'; +import _ from 'lodash'; +import { UI_DATE_FORMAT } from '../../../../../common/constants/shared'; +import React, { useEffect, useState } from 'react'; +import { FlyoutContainers } from '../../helpers/flyout_containers'; +import { displayVisualization, getQueryResponse, isDateValid } from '../../helpers/utils'; +import { convertDateTime } from '../../helpers/utils'; +import PPLService from '../../../../services/requests/ppl'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { CUSTOM_PANELS_API_PREFIX } from '../../../../../common/constants/custom_panels'; +import { + pplResponse, + SavedVisualizationType, + VisualizationType, +} from '../../../../../common/types/custom_panels'; +import './visualization_flyout.scss'; + +/* + * VisaulizationFlyout - This module create a flyout to add visualization + * + * Props taken in as params are: + * panelId: panel Id of current operational panel + * 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 + * http: http core service + * pplService: ppl requestor service + * setPanelVisualizations: function set the visualization list in panel + * isFlyoutReplacement: boolean to see if the flyout is trigger for add or replace visualization + * replaceVisualizationId: string id of the visualization to be replaced + */ + +type Props = { + panelId: string; + closeFlyout: () => void; + start: ShortDate; + end: ShortDate; + setToast: ( + title: string, + color?: string, + text?: React.ReactChild | undefined, + side?: string | undefined + ) => void; + http: CoreStart['http']; + pplService: PPLService; + setPanelVisualizations: React.Dispatch>; + isFlyoutReplacement?: boolean | undefined; + replaceVisualizationId?: string | undefined; +}; + +export const VisaulizationFlyout = ({ + panelId, + closeFlyout, + start, + end, + setToast, + http, + pplService, + setPanelVisualizations, + isFlyoutReplacement, + replaceVisualizationId, +}: Props) => { + const [newVisualizationTitle, setNewVisualizationTitle] = useState(''); + const [newVisualizationType, setNewVisualizationType] = useState(''); + const [newVisualizationTimeField, setNewVisualizationTimeField] = useState(''); + const [pplQuery, setPPLQuery] = useState(''); + const [previewData, setPreviewData] = useState({} as pplResponse); + const [previewArea, setPreviewArea] = useState(<>); + const [showPreviewArea, setShowPreviewArea] = useState(false); + const [previewIconType, setPreviewIconType] = useState('arrowRight'); + const [previewLoading, setPreviewLoading] = useState(false); + const [isPreviewError, setIsPreviewError] = useState(''); + const [savedVisualizations, setSavedVisualizations] = useState([]); + const [visualizationOptions, setVisualizationOptions] = useState([]); + const [selectValue, setSelectValue] = useState(''); + + // DateTimePicker States + const startDate = convertDateTime(start, true, false); + const endDate = convertDateTime(end, false, false); + + const onPreviewClick = () => { + if (previewIconType == 'arrowRight') { + setPreviewIconType('arrowUp'); + setShowPreviewArea(true); + } else { + setPreviewIconType('arrowRight'); + setShowPreviewArea(false); + } + }; + + const isInputValid = () => { + if (!isDateValid(convertDateTime(start), convertDateTime(end, false), setToast, 'left')) { + return false; + } + + if (selectValue === '') { + setToast('Please make a valid selection', 'danger', undefined, 'left'); + return false; + } + + return true; + }; + + const addVisualization = () => { + if (!isInputValid()) return; + + if (isFlyoutReplacement) { + http + .post(`${CUSTOM_PANELS_API_PREFIX}/visualizations/replace`, { + body: JSON.stringify({ + panelId: panelId, + oldVisualizationId: replaceVisualizationId, + newVisualization: { + id: 'panelViz_' + htmlIdGenerator()(), + title: newVisualizationTitle, + query: pplQuery, + type: newVisualizationType, + timeField: newVisualizationTimeField, + }, + }), + }) + .then(async (res) => { + setPanelVisualizations(res.visualizations); + setToast(`Visualization ${newVisualizationTitle} successfully added!`, 'success'); + }) + .catch((err) => { + setToast(`Error in adding ${newVisualizationTitle} visualization to the panel`, 'danger'); + console.error(err); + }); + } else { + http + .post(`${CUSTOM_PANELS_API_PREFIX}/visualizations`, { + body: JSON.stringify({ + panelId: panelId, + newVisualization: { + id: 'panelViz_' + htmlIdGenerator()(), + title: newVisualizationTitle, + query: pplQuery, + type: newVisualizationType, + timeField: newVisualizationTimeField, + }, + }), + }) + .then(async (res) => { + setPanelVisualizations(res.visualizations); + setToast(`Visualization ${newVisualizationTitle} successfully added!`, 'success'); + }) + .catch((err) => { + setToast(`Error in adding ${newVisualizationTitle} visualization to the panel`, 'danger'); + console.error(err); + }); + } + closeFlyout(); + }; + + const onRefreshPreview = () => { + if (!isInputValid()) return; + + getQueryResponse( + pplService, + pplQuery, + newVisualizationType, + start, + end, + setPreviewData, + setPreviewLoading, + setIsPreviewError, + '', + newVisualizationTimeField + ); + }; + + const timeRange = ( + + endDate} + aria-label="Start date" + dateFormat={UI_DATE_FORMAT} + /> + } + endDateControl={ + endDate} + aria-label="End date" + dateFormat={UI_DATE_FORMAT} + /> + } + /> + + ); + + const flyoutHeader = ( + + +

+ {isFlyoutReplacement ? 'Replace Visualization' : 'Select Existing Visualization'} +

+
+
+ ); + + const onChangeSelection = (e: React.ChangeEvent) => { + setSelectValue(e.target.value); + }; + + const emptySavedVisualizations = ( + +

No saved visualizations found!

+
+ ); + + const flyoutBody = + savedVisualizations.length > 0 ? ( + + <> + + + onChangeSelection(e)} + options={visualizationOptions} + /> + + + + + + + Preview + + + + + + + + {showPreviewArea && previewArea} + + + + ) : ( + + <> +
+ You don't have any saved visualizations. Please use the "create new visualization" + option in add visualization menu. +
+ +
+ ); + + const flyoutFooter = ( + + + + Cancel + + + + Add + + + + + ); + + // Fetch all saved visualizations + const fetchSavedVisualizations = async () => { + return http + .get(`${CUSTOM_PANELS_API_PREFIX}/visualizations`) + .then((res) => { + if (res.visualizations.length > 0) { + setSavedVisualizations(res.visualizations); + setVisualizationOptions( + res.visualizations.map((visualization: SavedVisualizationType) => { + return { value: visualization.id, text: visualization.name }; + }) + ); + } + }) + .catch((err) => { + console.error('Issue in fetching the operational panels', err); + }); + }; + + useEffect(() => { + const previewTemplate = ( + <> + {timeRange} + {previewLoading ? ( + + ) : isPreviewError != '' ? ( +
+ + + + +

Error in rendering the visualizaiton

+
+ + +

{isPreviewError}

+
+
+ ) : ( + + + {displayVisualization(previewData, newVisualizationType)} + + + )} + + ); + setPreviewArea(previewTemplate); + }, [previewLoading]); + + // On change of selected visualization change options + useEffect(() => { + for (var i = 0; i < savedVisualizations.length; i++) { + const visualization = savedVisualizations[i]; + if (visualization.id === selectValue) { + setPPLQuery(visualization.query); + setNewVisualizationTitle(visualization.name); + setNewVisualizationType(visualization.type); + setNewVisualizationTimeField(visualization.timeField); + break; + } + } + }, [selectValue]); + + // load saved visualizations + useEffect(() => { + fetchSavedVisualizations(); + }, []); + + return ( + + ); +}; diff --git a/public/components/visualizations/charts/bar.tsx b/public/components/visualizations/charts/bar.tsx index c5b1662d5..638630dc0 100644 --- a/public/components/visualizations/charts/bar.tsx +++ b/public/components/visualizations/charts/bar.tsx @@ -10,6 +10,7 @@ */ import React from 'react'; + import { take, merge @@ -60,4 +61,4 @@ export const Bar = ({ config={ barConfig } /> ); -}; \ No newline at end of file +}; diff --git a/public/components/visualizations/charts/horizontal_bar.tsx b/public/components/visualizations/charts/horizontal_bar.tsx index 0e33f39fc..325f38e0e 100644 --- a/public/components/visualizations/charts/horizontal_bar.tsx +++ b/public/components/visualizations/charts/horizontal_bar.tsx @@ -49,4 +49,4 @@ export const HorizontalBar = ({ layoutConfig={ horizontalBarLayoutConfig } /> ); -}; \ No newline at end of file +}; diff --git a/public/components/visualizations/charts/line.tsx b/public/components/visualizations/charts/line.tsx index 7a66eab21..1013bdc3d 100644 --- a/public/components/visualizations/charts/line.tsx +++ b/public/components/visualizations/charts/line.tsx @@ -55,11 +55,11 @@ export const Line = ({ yaxis: { fixedrange: true, showgrid: false, - visible: true + visible: true, }, ...lineLayoutConfig }} config={ lineConfig } /> ); -}; \ No newline at end of file +}; diff --git a/server/adaptors/custom_panels/custom_panel_adaptor.ts b/server/adaptors/custom_panels/custom_panel_adaptor.ts index 0d173aed2..f5992c712 100644 --- a/server/adaptors/custom_panels/custom_panel_adaptor.ts +++ b/server/adaptors/custom_panels/custom_panel_adaptor.ts @@ -9,12 +9,8 @@ * GitHub history for details. */ -import { - PanelType, - VisualizationType, -} from "../../../common/types/custom_panels"; -import { ILegacyScopedClusterClient } from "../../../../../src/core/server"; -import { PPL_CONTAINS_TIMESTAMP_REGEX } from "../../../common/constants/shared"; +import { PanelType, VisualizationType } from '../../../common/types/custom_panels'; +import { ILegacyScopedClusterClient } from '../../../../../src/core/server'; export class CustomPanelsAdaptor { // index a panel @@ -23,17 +19,14 @@ export class CustomPanelsAdaptor { panelBody: PanelType ): Promise<{ objectId: string }> { try { - const response = await client.callAsCurrentUser( - "observability.createObject", - { - body: { - operationalPanel: panelBody, - }, - } - ); + const response = await client.callAsCurrentUser('observability.createObject', { + body: { + operationalPanel: panelBody, + }, + }); return response; } catch (error) { - throw new Error("Index Panel Error:" + error); + throw new Error('Index Panel Error:' + error); } }; @@ -44,48 +37,36 @@ export class CustomPanelsAdaptor { updatePanelBody: Partial ) { try { - const response = await client.callAsCurrentUser( - "observability.updateObjectById", - { - objectId: panelId, - body: { - operationalPanel: updatePanelBody, - }, - } - ); + const response = await client.callAsCurrentUser('observability.updateObjectById', { + objectId: panelId, + body: { + operationalPanel: updatePanelBody, + }, + }); return response; } catch (error) { - throw new Error("Update Panel Error:" + error); + throw new Error('Update Panel Error:' + error); } }; // fetch a panel by id - getPanel = async function ( - client: ILegacyScopedClusterClient, - panelId: string - ) { + getPanel = async function (client: ILegacyScopedClusterClient, panelId: string) { try { - const response = await client.callAsCurrentUser( - "observability.getObjectById", - { - objectId: panelId, - } - ); + const response = await client.callAsCurrentUser('observability.getObjectById', { + objectId: panelId, + }); return response.observabilityObjectList[0]; } catch (error) { - throw new Error("Get Panel Error:" + error); + throw new Error('Get Panel Error:' + error); } }; // gets list of panels stored in index viewPanelList = async function (client: ILegacyScopedClusterClient) { try { - const response = await client.callAsCurrentUser( - "observability.getObject", - { - objectType: "operationalPanel", - } - ); + const response = await client.callAsCurrentUser('observability.getObject', { + objectType: 'operationalPanel', + }); return response.observabilityObjectList.map((panel: any) => ({ name: panel.operationalPanel.name, id: panel.objectId, @@ -93,61 +74,46 @@ export class CustomPanelsAdaptor { dateModified: panel.lastUpdatedTimeMs, })); } catch (error) { - throw new Error("View Panel List Error:" + error); + throw new Error('View Panel List Error:' + error); } }; // Delete a panel by Id - deletePanel = async function ( - client: ILegacyScopedClusterClient, - panelId: string - ) { + deletePanel = async function (client: ILegacyScopedClusterClient, panelId: string) { try { - const response = await client.callAsCurrentUser( - "observability.deleteObjectById", - { - objectId: panelId, - } - ); - return { status: "OK", message: response }; + const response = await client.callAsCurrentUser('observability.deleteObjectById', { + objectId: panelId, + }); + return { status: 'OK', message: response }; } catch (error) { - throw new Error("Delete Panel Error:" + error); + throw new Error('Delete Panel Error:' + error); } }; // Delete a panel by Id - deletePanelList = async function ( - client: ILegacyScopedClusterClient, - panelIdList: string - ) { + deletePanelList = async function (client: ILegacyScopedClusterClient, panelIdList: string) { try { - const response = await client.callAsCurrentUser( - "observability.deleteObjectByIdList", - { - objectIdList: panelIdList, - } - ); - return { status: "OK", message: response }; + const response = await client.callAsCurrentUser('observability.deleteObjectByIdList', { + objectIdList: panelIdList, + }); + return { status: 'OK', message: response }; } catch (error) { - throw new Error("Delete Panel List Error:" + error); + throw new Error('Delete Panel List Error:' + error); } }; // Create a new Panel - createNewPanel = async ( - client: ILegacyScopedClusterClient, - panelName: string - ) => { + createNewPanel = async (client: ILegacyScopedClusterClient, panelName: string) => { const panelBody = { name: panelName, visualizations: [], timeRange: { - to: "now", - from: "now-1d", + to: 'now', + from: 'now-1d', }, queryFilter: { - query: "", - language: "ppl", + query: '', + language: 'ppl', }, }; @@ -155,16 +121,12 @@ export class CustomPanelsAdaptor { const response = await this.indexPanel(client, panelBody); return response.objectId; } catch (error) { - throw new Error("Create New Panel Error:" + error); + throw new Error('Create New Panel Error:' + error); } }; // Rename an existing panel - renamePanel = async ( - client: ILegacyScopedClusterClient, - panelId: string, - panelName: string - ) => { + renamePanel = async (client: ILegacyScopedClusterClient, panelId: string, panelName: string) => { const updatePanelBody = { name: panelName, }; @@ -172,16 +134,12 @@ export class CustomPanelsAdaptor { const response = await this.updatePanel(client, panelId, updatePanelBody); return response.objectId; } catch (error) { - throw new Error("Rename Panel Error:" + error); + throw new Error('Rename Panel Error:' + error); } }; // Clone an existing panel - clonePanel = async ( - client: ILegacyScopedClusterClient, - panelId: string, - panelName: string - ) => { + clonePanel = async (client: ILegacyScopedClusterClient, panelId: string, panelName: string) => { const updatePanelBody = { name: panelName, }; @@ -194,17 +152,14 @@ export class CustomPanelsAdaptor { queryFilter: getPanel.operationalPanel.queryFilter, }; const indexResponse = await this.indexPanel(client, clonePanelBody); - const getClonedPanel = await this.getPanel( - client, - indexResponse.objectId - ); + const getClonedPanel = await this.getPanel(client, indexResponse.objectId); return { clonePanelId: getClonedPanel.objectId, dateCreated: getClonedPanel.createdTimeMs, dateModified: getClonedPanel.lastUpdatedTimeMs, }; } catch (error) { - throw new Error("Clone Panel Error:" + error); + throw new Error('Clone Panel Error:' + error); } }; @@ -231,64 +186,38 @@ export class CustomPanelsAdaptor { const response = await this.updatePanel(client, panelId, updatePanelBody); return response.objectId; } catch (error) { - throw new Error("Add Panel Filter Error:" + error); + throw new Error('Add Panel Filter Error:' + error); } }; - // Check for time filter in query - checkTimeRangeExists = (query: string) => { - return PPL_CONTAINS_TIMESTAMP_REGEX.test(query); - }; - - // savedObjects Visualzation Query Builder - // removes time filter from query - // NOTE: this is a separate function to add more fields for future releases - savedVisualizationsQueryBuilder = (query: string) => { - return this.checkTimeRangeExists(query) - ? query.replace(PPL_CONTAINS_TIMESTAMP_REGEX, "") - : query; - }; - // gets list of panels stored in index viewSavedVisualiationList = async (client: ILegacyScopedClusterClient) => { try { - const response = await client.callAsCurrentUser( - "observability.getObject", - { - objectType: "savedVisualization", - } - ); + const response = await client.callAsCurrentUser('observability.getObject', { + objectType: 'savedVisualization', + }); return response.observabilityObjectList.map((visualization: any) => ({ id: visualization.objectId, name: visualization.savedVisualization.name, - query: this.savedVisualizationsQueryBuilder( - visualization.savedVisualization.query - ), + query: visualization.savedVisualization.query, type: visualization.savedVisualization.type, timeField: visualization.savedVisualization.selected_timestamp.name, })); } catch (error) { - throw new Error("View Saved Visualizations Error:" + error); + throw new Error('View Saved Visualizations Error:' + error); } }; //Get All Visualizations from a Panel //Add Visualization - getVisualizations = async ( - client: ILegacyScopedClusterClient, - panelId: string - ) => { + getVisualizations = async (client: ILegacyScopedClusterClient, panelId: string) => { try { - const response = await client.callAsCurrentUser( - "observability.getObjectById", - { - objectId: panelId, - } - ); - return response.observabilityObjectList[0].operationalPanel - .visualizations; + const response = await client.callAsCurrentUser('observability.getObjectById', { + objectId: panelId, + }); + return response.observabilityObjectList[0].operationalPanel.visualizations; } catch (error) { - throw new Error("Get Visualizations Error:" + error); + throw new Error('Get Visualizations Error:' + error); } }; @@ -322,10 +251,7 @@ export class CustomPanelsAdaptor { oldVisualizationId?: string ) => { try { - const allPanelVisualizations = await this.getVisualizations( - client, - panelId - ); + const allPanelVisualizations = await this.getVisualizations(client, panelId); let newDimensions; let visualizationsList = []; @@ -355,7 +281,7 @@ export class CustomPanelsAdaptor { }); return newPanelVisualizations; } catch (error) { - throw new Error("Add/Replace Visualization Error:" + error); + throw new Error('Add/Replace Visualization Error:' + error); } }; @@ -372,25 +298,18 @@ export class CustomPanelsAdaptor { } ) => { try { - const allPanelVisualizations = await this.getVisualizations( - client, - panelId - ); - const newVisualization = { - ...paramVisualization, - query: this.savedVisualizationsQueryBuilder(paramVisualization.query), - }; + const allPanelVisualizations = await this.getVisualizations(client, panelId); const newDimensions = this.getNewVizDimensions(allPanelVisualizations); const newPanelVisualizations = [ ...allPanelVisualizations, - { ...newVisualization, ...newDimensions }, + { ...paramVisualization, ...newDimensions }, ]; const updatePanelResponse = await this.updatePanel(client, panelId, { visualizations: newPanelVisualizations, }); return newPanelVisualizations; } catch (error) { - throw new Error("Add/Replace Visualization Error:" + error); + throw new Error('Add/Replace Visualization Error:' + error); } }; @@ -401,20 +320,16 @@ export class CustomPanelsAdaptor { visualizationId: string ) => { try { - const allPanelVisualizations = await this.getVisualizations( - client, - panelId - ); + const allPanelVisualizations = await this.getVisualizations(client, panelId); const filteredPanelVisualizations = allPanelVisualizations.filter( - (panelVisualization: VisualizationType) => - panelVisualization.id != visualizationId + (panelVisualization: VisualizationType) => panelVisualization.id != visualizationId ); const updatePanelResponse = await this.updatePanel(client, panelId, { visualizations: filteredPanelVisualizations, }); return filteredPanelVisualizations; } catch (error) { - throw new Error("Delete Visualization Error:" + error); + throw new Error('Delete Visualization Error:' + error); } }; @@ -431,10 +346,7 @@ export class CustomPanelsAdaptor { }[] ) => { try { - const allPanelVisualizations = await this.getVisualizations( - client, - panelId - ); + const allPanelVisualizations = await this.getVisualizations(client, panelId); let filteredPanelVisualizations = >[]; for (let i = 0; i < allPanelVisualizations.length; i++) { @@ -455,7 +367,7 @@ export class CustomPanelsAdaptor { }); return filteredPanelVisualizations; } catch (error) { - throw new Error("Edit Visualizations Error:" + error); + throw new Error('Edit Visualizations Error:' + error); } }; } diff --git a/server/routes/custom_panels/panels_router.ts b/server/routes/custom_panels/panels_router.ts index 9b2afd2b0..e999dbf2f 100644 --- a/server/routes/custom_panels/panels_router.ts +++ b/server/routes/custom_panels/panels_router.ts @@ -105,7 +105,6 @@ export function PanelsRouter(router: IRouter) { request, response ): Promise> => { - const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( request ); @@ -234,12 +233,50 @@ export function PanelsRouter(router: IRouter) { const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( request ); - const panelId = request.params.panelId; try { const deleteResponse = await customPanelBackend.deletePanel( opensearchNotebooksClient, - panelId + request.params.panelId + ); + return response.noContent({ + body: { + message: 'Panel Deleted', + }, + }); + } catch (error) { + console.error('Issue in deleting panel', error); + return response.custom({ + statusCode: error.statusCode || 500, + body: error.message, + }); + } + } + ); + + // delete an existing panel(s) + router.delete( + { + path: `${API_PREFIX}/panelList/{panelIdList}`, + validate: { + params: schema.object({ + panelIdList: schema.string(), + }), + }, + }, + async ( + context, + request, + response + ): Promise> => { + const opensearchNotebooksClient: ILegacyScopedClusterClient = context.observability_plugin.observabilityClient.asScoped( + request + ); + + try { + const deleteResponse = await customPanelBackend.deletePanelList( + opensearchNotebooksClient, + request.params.panelIdList ); return response.noContent({ body: {