/* * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ /* // eslint-disable no-console */ /* eslint-disable react-hooks/exhaustive-deps */ import { EuiBreadcrumb, EuiButton, EuiContextMenu, EuiContextMenuPanelDescriptor, EuiFlexGroup, EuiFlexItem, EuiLink, EuiOverlayMask, EuiPage, EuiPageBody, EuiPageContentBody, EuiPageHeader, EuiPageHeaderSection, EuiPopover, EuiSpacer, EuiSuperDatePicker, EuiTitle, OnTimeChangeProps, ShortDate, } from '@elastic/eui'; import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; import { last } from 'lodash'; import moment from 'moment'; import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { CoreStart } from '../../../../../src/core/public'; import { CREATE_PANEL_MESSAGE } from '../../../common/constants/custom_panels'; import { UI_DATE_FORMAT } from '../../../common/constants/shared'; import { CustomPanelType } from '../../../common/types/custom_panels'; import { uiSettingsService } from '../../../common/utils'; import { coreRefs } from '../../framework/core_refs'; import { PPLReferenceFlyout } from '../common/helpers'; import { DeleteModal } from '../common/helpers/delete_modal'; import { Autocomplete } from '../common/search/autocomplete'; import { onItemSelect, parseGetSuggestions } from '../common/search/autocomplete_logic'; import { addVisualizationPanel } from './helpers/add_visualization_helper'; import { AddVisualizationPopover } from './helpers/add_visualization_popover'; import { getCustomModal } from './helpers/modal_containers'; import { convertDateTime, isDateValid, isNameValid, isPPLFilterValid, prependRecentlyUsedRange, } from './helpers/utils'; import { EmptyPanelView } from './panel_modules/empty_panel'; import { PanelGridSO } from './panel_modules/panel_grid/panel_grid_so'; import { VisaulizationFlyoutSO } from './panel_modules/visualization_flyout/visualization_flyout_so'; import { clonePanel, deletePanels, fetchPanel, renameCustomPanel, selectPanel, setPanel, updatePanel, } from './redux/panel_slice'; import { useToast } from '../common/toast'; import PPLService from '../../services/requests/ppl'; import DSLService from '../../services/requests/dsl'; /* * "CustomPanelsView" module used to render an Observability Dashboard * * Props taken in as params are: * panelId: Name of the panel opened * page: Page where component is called * http: http core service * coreSavedObjects : savedObjects core service * pplService: ppl requestor service * dslService: dsl requestor service * chrome: chrome core service * parentBreadcrumb: parent breadcrumb * renameCustomPanel: Rename function for the panel * deleteCustomPanel: Delete function for the panel * cloneCustomPanel: Clone function for the panel * onEditClick: Edit function for visualization * startTime: Starting time * endTime: Ending time * setStartTime: Function to change start time * setEndTime: Function to change end time * childBreadcrumbs: Breadcrumbs to extend * appId: id of application that panel belongs to * onAddClick: Function for add button instead of add visualization popover */ interface CustomPanelViewProps { panelId: string; page: 'app' | 'operationalPanels'; coreSavedObjects: CoreStart['savedObjects']; chrome: CoreStart['chrome']; parentBreadcrumbs: EuiBreadcrumb[]; cloneCustomPanel: (clonedCustomPanelName: string, clonedCustomPanelId: string) => Promise; onEditClick: (savedVisualizationId: string) => any; childBreadcrumbs?: EuiBreadcrumb[]; appId?: string; updateAvailabilityVizId?: any; onAddClick?: any; pplService: PPLService; dslService: DSLService; } export const CustomPanelViewSO = (props: CustomPanelViewProps) => { const { panelId, page, appId, pplService, dslService, chrome, parentBreadcrumbs, childBreadcrumbs, updateAvailabilityVizId, cloneCustomPanel, onEditClick, onAddClick, } = props; const dispatch = useDispatch(); const { setToast } = useToast(); const panel = useSelector(selectPanel); const [loading, setLoading] = useState(true); const [pplFilterValue, setPPLFilterValue] = useState(''); const [baseQuery, setBaseQuery] = useState(''); const [onRefresh, setOnRefresh] = useState(false); const [inputDisabled, setInputDisabled] = useState(true); const [addVizDisabled, setAddVizDisabled] = useState(false); const [editDisabled, setEditDisabled] = useState(false); const [dateDisabled, setDateDisabled] = useState(false); const [isEditing, setIsEditing] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false); // Modal Toggle const [modalLayout, setModalLayout] = useState(); // Modal Layout const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); // Add Visualization Flyout const [isFlyoutReplacement, setisFlyoutReplacement] = useState(false); const [replaceVisualizationId, setReplaceVisualizationId] = useState(''); const [panelsMenuPopover, setPanelsMenuPopover] = useState(false); const [editActionType, setEditActionType] = useState(''); const [isHelpFlyoutVisible, setHelpIsFlyoutVisible] = useState(false); const appPanel = page === 'app'; const closeHelpFlyout = () => { setAddVizDisabled(false); setHelpIsFlyoutVisible(false); }; const showHelpFlyout = () => { setAddVizDisabled(true); setHelpIsFlyoutVisible(true); }; // DateTimePicker States/add const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([]); const handleQueryChange = (newQuery: string) => { setPPLFilterValue(newQuery); }; const closeModal = () => { setIsModalVisible(false); }; const showModal = () => { setIsModalVisible(true); }; const onDatePickerChange = (timeProps: OnTimeChangeProps) => { const updatedRanges = prependRecentlyUsedRange( timeProps.start, timeProps.end, recentlyUsedRanges ); dispatch(updatePanel({ ...panel, timeRange: { from: timeProps.start, to: timeProps.end } }, '', '')); setRecentlyUsedRanges(updatedRanges.slice(0, 9)); onRefreshFilters(timeProps.start, timeProps.end); }; const onDelete = async () => { dispatch(deletePanels([panel])); setTimeout(() => { window.location.assign(`${last(parentBreadcrumbs)!.href}`); }, 1000); closeModal(); }; const deletePanel = () => { setModalLayout( ); showModal(); }; const onRename = async (newCustomPanelName: string) => { if (!isNameValid(newCustomPanelName)) { setToast('Invalid Dashboard name', 'danger'); } else { dispatch(renameCustomPanel(newCustomPanelName, panel.id)); } closeModal(); }; const renamePanel = () => { setModalLayout( getCustomModal( onRename, closeModal, 'Name', 'Rename Dashboard', 'Cancel', 'Rename', panel.title, CREATE_PANEL_MESSAGE ) ); showModal(); }; const onClone = async (newCustomPanelName: string) => { if (!isNameValid(newCustomPanelName)) { setToast('Invalid Operational Panel name', 'danger'); } else { dispatch(clonePanel(panel, newCustomPanelName)); } closeModal(); }; const clonePanelModal = () => { setModalLayout( getCustomModal( onClone, closeModal, 'Name', 'Duplicate Dashboard', 'Cancel', 'Duplicate', panel.title + ' (copy)', CREATE_PANEL_MESSAGE ) ); showModal(); }; // toggle between panel edit mode const editPanel = (editType: string) => { setIsEditing(!isEditing); if (editType === 'cancel') dispatch(fetchPanel(panelId)); setEditActionType(editType); }; const closeFlyout = () => { setIsFlyoutVisible(false); setAddVizDisabled(false); checkDisabledInputs(); }; const showFlyout = (isReplacement?: boolean, replaceVizId?: string) => { setisFlyoutReplacement(isReplacement); setReplaceVisualizationId(replaceVizId); setIsFlyoutVisible(true); setAddVizDisabled(true); setInputDisabled(true); }; const checkDisabledInputs = () => { // When not in edit mode and panel has no visualizations if (panel.visualizations.length === 0 && !isEditing) { setEditDisabled(true); setInputDisabled(true); setAddVizDisabled(false); setDateDisabled(false); } // When panel has visualizations if (panel.visualizations.length > 0) { setEditDisabled(false); setInputDisabled(false); setAddVizDisabled(false); setDateDisabled(false); } // When in edit mode if (isEditing) { setEditDisabled(false); setInputDisabled(true); setAddVizDisabled(true); setDateDisabled(true); } }; const buildBaseQuery = async () => { // const indices: string[] = []; // for (let i = 0; i < visualizations.length; i++) { // const visualizationId = visualizations[i].savedVisualizationId; // // TODO: create route to get list of visualizations in one call // const visData: SavedVisualizationType = await fetchVisualizationById( // http, // visualizationId, // (error: VizContainerError) => setToast(error.errorMessage, 'danger') // ); // if (!_.isEmpty(visData)) { // const moreIndices = parseForIndices(visData.query); // for (let j = 0; j < moreIndices.length; j++) { // if (!indices.includes(moreIndices[j])) { // indices.push(moreIndices[j]); // } // } // } // } // setBaseQuery('source = ' + indices.join(', ')); return; }; const onRefreshFilters = async (start: ShortDate, end: ShortDate) => { if (!isDateValid(convertDateTime(start), convertDateTime(end, false), setToast)) { return; } if (!isPPLFilterValid(pplFilterValue, setToast)) { console.error(pplFilterValue); return; } await coreRefs.savedObjectsClient?.update('observability-panel', panelId, { ...panel, timeRange: { to: end, from: start, }, queryFilter: { query: pplFilterValue, language: 'ppl', }, }); setOnRefresh(!onRefresh); }; const cloneVisualization = (visualzationTitle: string, savedVisualizationId: string) => { addVisualizationToCurrentPanel({ savedVisualizationId, successMsg: `Visualization ${visualzationTitle} successfully added!`, failureMsg: `Error in adding ${visualzationTitle} visualization to the panel` }); }; const cancelButton = ( editPanel('cancel')} > Cancel ); const saveButton = ( editPanel('save')}> Save ); const editButton = ( editPanel('edit')} disabled={editDisabled} > Edit ); const addButton = ( Add ); // Panel Actions Button const panelActionsButton = ( setPanelsMenuPopover(true)} disabled={addVizDisabled} > Dashboard Actions ); const addVisualizationToCurrentPanel = async ({ savedVisualizationId, oldVisualizationId, successMsg, failureMsg, }: { savedVisualizationId: string; oldVisualizationId?: string; successMsg: string; failureMsg: string; }) => { const allVisualizations = panel!.visualizations; const visualizationsWithNewPanel = addVisualizationPanel( savedVisualizationId, oldVisualizationId, allVisualizations ); const updatedPanel = { ...panel, visualizations: visualizationsWithNewPanel }; dispatch(updatePanel(updatedPanel, successMsg, failureMsg)); }; const setPanelVisualizations = (newVis) => { const newPanel: CustomPanelType = { ...panel, visualizations: newVis }; dispatch(setPanel(newPanel)); }; let flyout; if (isFlyoutVisible) { flyout = ( ); } let helpFlyout; if (isHelpFlyoutVisible) { helpFlyout = ; } const panelActionsMenu: EuiContextMenuPanelDescriptor[] = [ { id: 0, title: 'Panel actions', items: [ { name: 'Reload Dashboard', 'data-test-subj': 'reloadPanelContextMenuItem', onClick: () => { setPanelsMenuPopover(false); dispatch(fetchPanel(panelId)); }, }, { name: 'Rename Dashboard', 'data-test-subj': 'renamePanelContextMenuItem', onClick: () => { setPanelsMenuPopover(false); renamePanel(); }, }, { name: 'Duplicate Dashboard', 'data-test-subj': 'duplicatePanelContextMenuItem', onClick: () => { setPanelsMenuPopover(false); clonePanelModal(); }, }, { name: 'Delete Dashboard', 'data-test-subj': 'deletePanelContextMenuItem', onClick: () => { setPanelsMenuPopover(false); deletePanel(); }, }, ], }, ]; // Fetch the Observability Dashboard on Initial Mount useEffect(() => { setLoading(true); dispatch(fetchPanel(panelId)); }, []); // Toggle input type (disabled or not disabled) // Disabled when there no visualizations in panels or when the panel is in edit mode useEffect(() => { if (!loading) { checkDisabledInputs(); } }, [isEditing, loading]); // Build base query with all of the indices included in the current visualizations useEffect(() => { if (loading) { if (panel.id === props.panelId) setLoading(false); else return; } checkDisabledInputs(); buildBaseQuery(); setLoading(false); }, [panel, loading]); // Edit the breadcrumb when panel name changes useEffect(() => { if (!panel) return; let newBreadcrumb; if (childBreadcrumbs) { newBreadcrumb = childBreadcrumbs; } else { newBreadcrumb = [ { text: panel.title, href: `${last(parentBreadcrumbs)!.href}${panelId}`, }, ]; } chrome.setBreadcrumbs([...parentBreadcrumbs, ...newBreadcrumb]); }, [panelId, panel]); return loading ? ( <> ) : (
{appPanel || ( <>

{panel?.title}

Created on {moment(panel?.dateCreated || 0).format(UI_DATE_FORMAT)}
{isEditing ? ( <> {cancelButton} {saveButton} ) : ( {editButton} )} setPanelsMenuPopover(false)} > )}
onRefreshFilters(panel.timeRange.from, panel.timeRange.to) } dslService={dslService} getSuggestions={parseGetSuggestions} onItemSelect={onItemSelect} isDisabled={inputDisabled} tabId={'panels-filter'} placeholder={ "Use PPL 'where' clauses to add filters on all visualizations [where Carrier = 'OpenSearch-Air']" } possibleCommands={[{ label: 'where' }]} append={ PPL } /> {appPanel && ( <> {isEditing ? ( <> {cancelButton} {saveButton} ) : ( {editButton} )} {addButton} )} {panel.visualizations.length === 0 && ( )}
{isModalVisible && modalLayout} {flyout} {helpFlyout}
); };