diff --git a/dashboards-observability/common/constants/metrics.ts b/dashboards-observability/common/constants/metrics.ts index 370bd47e1..49232392f 100644 --- a/dashboards-observability/common/constants/metrics.ts +++ b/dashboards-observability/common/constants/metrics.ts @@ -9,3 +9,16 @@ export const PPL_PROMETHEUS_CATALOG_REQUEST = // redux export const REDUX_SLICE_METRICS = 'metrics'; + +export const resolutionOptions = [ + { value: 's', text: 'seconds' }, + { value: 'm', text: 'minutes' }, + { value: 'h', text: 'hours' }, + { value: 'd', text: 'days' }, + { value: 'M', text: 'Months' }, + { value: 'q', text: 'quarters' }, + { value: 'y', text: 'years' }, +]; + +export const DEFAULT_METRIC_HEIGHT = 2; +export const DEFAULT_METRIC_WIDTH = 12; diff --git a/dashboards-observability/common/types/metrics.ts b/dashboards-observability/common/types/metrics.ts new file mode 100644 index 000000000..64ebd07ab --- /dev/null +++ b/dashboards-observability/common/types/metrics.ts @@ -0,0 +1,22 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VisualizationType } from './custom_panels'; + +export interface MetricData { + metricId: string; + metricType: 'savedCustomMetric' | 'prometheusMetric'; + metricName: string; +} + +export interface MetricType extends VisualizationType { + id: string; + savedVisualizationId: string; + x: number; + y: number; + w: number; + h: number; + metricType: 'savedCustomMetric' | 'prometheusMetric'; +} diff --git a/dashboards-observability/public/components/custom_panels/helpers/utils.tsx b/dashboards-observability/public/components/custom_panels/helpers/utils.tsx index 5f3c67ac9..483361dad 100644 --- a/dashboards-observability/public/components/custom_panels/helpers/utils.tsx +++ b/dashboards-observability/public/components/custom_panels/helpers/utils.tsx @@ -88,7 +88,8 @@ const queryAccumulator = ( timestampField: string, startTime: string, endTime: string, - panelFilterQuery: string + panelFilterQuery: string, + spanParam: string | undefined ) => { const indexMatchArray = originalQuery.match(PPL_INDEX_REGEX); if (indexMatchArray == null) { @@ -100,7 +101,15 @@ const queryAccumulator = ( startTime )}' and ${timestampField} <= '${convertDateTime(endTime, false)}'`; const pplFilterQuery = panelFilterQuery === '' ? '' : ` | ${panelFilterQuery}`; - return indexPartOfQuery + timeQueryFilter + pplFilterQuery + filterPartOfQuery; + const finalQuery = indexPartOfQuery + timeQueryFilter + pplFilterQuery + filterPartOfQuery; + if (spanParam === undefined) { + return finalQuery; + } else { + return finalQuery.replace( + new RegExp(`span\\(${timestampField},(.*?)\\)`), + `span(${timestampField},${spanParam})` + ); + } }; // PPL Service requestor @@ -154,6 +163,7 @@ export const getQueryResponse = ( type: string, startTime: string, endTime: string, + spanParam: string | undefined, setVisualizationData: React.Dispatch>, setIsLoading: React.Dispatch>, setIsError: React.Dispatch>, @@ -165,7 +175,14 @@ export const getQueryResponse = ( let finalQuery = ''; try { - finalQuery = queryAccumulator(query, timestampField, startTime, endTime, filterQuery); + finalQuery = queryAccumulator( + query, + timestampField, + startTime, + endTime, + filterQuery, + spanParam + ); } catch (error) { const errorMessage = 'Issue in building final query'; setIsError(errorMessage); @@ -185,6 +202,7 @@ export const renderSavedVisualization = async ( startTime: string, endTime: string, filterQuery: string, + spanParam: string | undefined, setVisualizationTitle: React.Dispatch>, setVisualizationType: React.Dispatch>, setVisualizationData: React.Dispatch>, @@ -219,6 +237,7 @@ export const renderSavedVisualization = async ( visualization.type, startTime, endTime, + spanParam, setVisualizationData, setIsLoading, setIsError, @@ -227,6 +246,83 @@ export const renderSavedVisualization = async ( ); }; +const createCatalogVisualizationMetaData = ( + catalogSource: string, + visualizationQuery: string, + visualizationType: string, + visualizationTimeField: string +) => { + return { + name: catalogSource, + description: '', + query: visualizationQuery, + type: visualizationType, + selected_date_range: { + start: 'now/y', + end: 'now', + text: '', + }, + selected_timestamp: { + name: visualizationTimeField, + type: 'timestamp', + }, + selected_fields: { + text: '', + tokens: [], + }, + }; +}; + +//Creates a catalogVisualization for a runtime catalog based PPL query and runs getQueryResponse +export const renderCatalogVisualization = async ( + http: CoreStart['http'], + pplService: PPLService, + catalogSource: string, + startTime: string, + endTime: string, + filterQuery: string, + spanParam: string | undefined, + setVisualizationTitle: React.Dispatch>, + setVisualizationType: React.Dispatch>, + setVisualizationData: React.Dispatch>, + setVisualizationMetaData: React.Dispatch>, + setIsLoading: React.Dispatch>, + setIsError: React.Dispatch>, + spanResolution?: string +) => { + setIsLoading(true); + setIsError(''); + + const visualizationType = 'line'; + const visualizationTimeField = '@timestamp'; + const visualizationQuery = `source = ${catalogSource} | stats avg(@value) by span(${visualizationTimeField},1h)`; + + const visualizationMetaData = createCatalogVisualizationMetaData( + catalogSource, + visualizationQuery, + visualizationType, + visualizationTimeField + ); + setVisualizationTitle(catalogSource); + setVisualizationType(visualizationType); + + setVisualizationMetaData(visualizationMetaData); + + getQueryResponse( + pplService, + visualizationQuery, + visualizationType, + startTime, + endTime, + spanParam, + setVisualizationData, + setIsLoading, + setIsError, + filterQuery, + visualizationTimeField + ); +}; + // Function to store recently used time filters and set start and end time. export const onTimeChange = ( start: ShortDate, diff --git a/dashboards-observability/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx b/dashboards-observability/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx index bb1a25e04..d89b7d6e9 100644 --- a/dashboards-observability/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx +++ b/dashboards-observability/public/components/custom_panels/panel_modules/visualization_container/visualization_container.tsx @@ -20,7 +20,11 @@ import { import React, { useEffect, useMemo, useState } from 'react'; import { CoreStart } from '../../../../../../../src/core/public'; import PPLService from '../../../../services/requests/ppl'; -import { displayVisualization, renderSavedVisualization } from '../../helpers/utils'; +import { + displayVisualization, + renderCatalogVisualization, + renderSavedVisualization, +} from '../../helpers/utils'; import './visualization_container.scss'; /* @@ -40,6 +44,8 @@ import './visualization_container.scss'; * pplFilterValue: string with panel PPL filter value * showFlyout: function to show the flyout * removeVisualization: function to remove all the visualizations + * catalogVisualization: boolean pointing if the container is used for catalog metrics + * spanParam: Override the span(timestamp, 1h) in visualization to span(timestamp, spanParam) */ interface Props { @@ -57,6 +63,8 @@ interface Props { cloneVisualization?: (visualzationTitle: string, savedVisualizationId: string) => void; showFlyout?: (isReplacement?: boolean | undefined, replaceVizId?: string | undefined) => void; removeVisualization?: (visualizationId: string) => void; + catalogVisualization?: boolean; + spanParam?: string; } export const VisualizationContainer = ({ @@ -74,6 +82,8 @@ export const VisualizationContainer = ({ cloneVisualization, showFlyout, removeVisualization, + catalogVisualization, + spanParam, }: Props) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); const [disablePopover, setDisablePopover] = useState(false); @@ -125,20 +135,38 @@ export const VisualizationContainer = ({ } const loadVisaulization = async () => { - await renderSavedVisualization( - http, - pplService, - savedVisualizationId, - fromTime, - toTime, - pplFilterValue, - setVisualizationTitle, - setVisualizationType, - setVisualizationData, - setVisualizationMetaData, - setIsLoading, - setIsError - ); + if (catalogVisualization) + await renderCatalogVisualization( + http, + pplService, + savedVisualizationId, + fromTime, + toTime, + pplFilterValue, + spanParam, + setVisualizationTitle, + setVisualizationType, + setVisualizationData, + setVisualizationMetaData, + setIsLoading, + setIsError + ); + else + await renderSavedVisualization( + http, + pplService, + savedVisualizationId, + fromTime, + toTime, + pplFilterValue, + spanParam, + setVisualizationTitle, + setVisualizationType, + setVisualizationData, + setVisualizationMetaData, + setIsLoading, + setIsError + ); }; const memoisedVisualizationBox = useMemo( diff --git a/dashboards-observability/public/components/metrics/helpers/utils.tsx b/dashboards-observability/public/components/metrics/helpers/utils.tsx index 11804262d..1f8a4583c 100644 --- a/dashboards-observability/public/components/metrics/helpers/utils.tsx +++ b/dashboards-observability/public/components/metrics/helpers/utils.tsx @@ -2,7 +2,6 @@ * Copyright OpenSearch Contributors * SPDX-License-Identifier: Apache-2.0 */ -/* eslint-disable no-console */ import dateMath from '@elastic/datemath'; import { ShortDate } from '@elastic/eui'; @@ -11,9 +10,13 @@ import _ from 'lodash'; import { Moment } from 'moment-timezone'; import React from 'react'; import { CUSTOM_PANELS_API_PREFIX } from '../../../../common/constants/custom_panels'; -import { PPL_DATE_FORMAT, PPL_INDEX_REGEX } from '../../../../common/constants/shared'; +import { PPL_DATE_FORMAT } from '../../../../common/constants/shared'; import PPLService from '../../../services/requests/ppl'; import { CoreStart } from '../../../../../../src/core/public'; +import { MetricType } from '../../../../common/types/metrics'; +import { Layout } from 'react-grid-layout'; +import { VisualizationType } from '../../../../common/types/custom_panels'; +import { DEFAULT_METRIC_HEIGHT, DEFAULT_METRIC_WIDTH } from '../../../../common/constants/metrics'; export const convertDateTime = (datetime: string, isStart = true, formatted = true) => { let returnTime: undefined | Moment; @@ -58,3 +61,108 @@ export const getVisualizations = (http: CoreStart['http']) => { console.error('Issue in fetching all saved visualizations', err); }); }; + +interface boxType { + x1: number; + y1: number; + x2: number; + y2: number; +} + +const calculatOverlapArea = (bb1: boxType, bb2: boxType) => { + const x_left = Math.max(bb1.x1, bb2.x1); + const y_top = Math.max(bb1.y1, bb2.y1); + const x_right = Math.min(bb1.x2, bb2.x2); + const y_bottom = Math.min(bb1.y2, bb2.y2); + + if (x_right < x_left || y_bottom < y_top) return 0; + return (x_right - x_left) * (y_bottom - y_top); +}; + +const getTotalOverlapArea = (panelVisualizations: MetricType[]) => { + const newVizBox = { x1: 0, y1: 0, x2: DEFAULT_METRIC_WIDTH, y2: DEFAULT_METRIC_HEIGHT }; + const currentVizBoxes = panelVisualizations.map((visualization) => { + return { + x1: visualization.x, + y1: visualization.y, + x2: visualization.x + visualization.w, + y2: visualization.y + visualization.h, + }; + }); + + let isOverlapping = 0; + currentVizBoxes.map((viz) => { + isOverlapping += calculatOverlapArea(viz, newVizBox); + }); + return isOverlapping; +}; + +// We want to check if the new visualization being added, can be placed at { x: 0, y: 0, w: 6, h: 4 }; +// To check this we try to calculate overlap between all the current visualizations and new visualization +// if there is no overalap (i.e Total Overlap Area is 0), we place the new viz. in default position +// else, we add it to the bottom of the panel +export const getNewVizDimensions = (panelVisualizations: MetricType[]) => { + let maxY: number = 0; + let maxYH: number = 0; + + // check if we can place the new visualization at default location + if (getTotalOverlapArea(panelVisualizations) === 0) { + return { x: 0, y: 0, w: DEFAULT_METRIC_WIDTH, h: DEFAULT_METRIC_HEIGHT }; + } + + // else place the new visualization at the bottom of the panel + panelVisualizations.map((panelVisualization: MetricType) => { + if (panelVisualization.y >= maxY) { + maxY = panelVisualization.y; + maxYH = panelVisualization.h; + } + }); + + return { x: 0, y: maxY + maxYH, w: DEFAULT_METRIC_WIDTH, h: DEFAULT_METRIC_HEIGHT }; +}; + +export const getMinSpanInterval = (start: any, end: any) => { + const momentStart = dateMath.parse(start)!; + const momentEnd = dateMath.parse(end, { roundUp: true })!; + const diffSeconds = momentEnd.unix() - momentStart.unix(); + let minInterval; + // // less than 1 second + // if (diffSeconds <= 1) minInterval = 'ms'; + // less than 2 minutes + if (diffSeconds <= 60 * 2) minInterval = 's'; + // less than 2 hours + else if (diffSeconds <= 3600 * 2) minInterval = 'm'; + // less than 2 days + else if (diffSeconds <= 86400 * 2) minInterval = 'h'; + // less than 1 month + else if (diffSeconds <= 86400 * 31) minInterval = 'd'; + // less than 3 months + else if (diffSeconds <= 86400 * 93) minInterval = 'w'; + // less than 1 year + else if (diffSeconds <= 86400 * 366) minInterval = 'M'; + + return minInterval; +}; + +// Merges new layout into visualizations +export const mergeLayoutAndMetrics = ( + layout: Layout[], + newVisualizationList: VisualizationType[] +) => { + const newPanelVisualizations: VisualizationType[] = []; + + for (let i = 0; i < newVisualizationList.length; i++) { + for (let j = 0; j < layout.length; j++) { + if (newVisualizationList[i].id == layout[j].i) { + newPanelVisualizations.push({ + ...newVisualizationList[i], + x: layout[j].x, + y: layout[j].y, + w: layout[j].w, + h: layout[j].h, + }); + } + } + } + return newPanelVisualizations; +}; diff --git a/dashboards-observability/public/components/metrics/index.tsx b/dashboards-observability/public/components/metrics/index.tsx index 9287001fe..e44b96d92 100644 --- a/dashboards-observability/public/components/metrics/index.tsx +++ b/dashboards-observability/public/components/metrics/index.tsx @@ -8,33 +8,26 @@ import { EuiButtonIcon, EuiPage, EuiPageBody, - EuiSpacer, - EuiSuperDatePicker, - EuiSuperDatePickerProps, - EuiText, - EuiTitle, - EuiTabbedContent, - OnRefreshChangeProps, - OnRefreshProps, + htmlIdGenerator, OnTimeChangeProps, ShortDate, - EuiPageContentBody, - EuiFlexGroup, - EuiFlexItem, } from '@elastic/eui'; import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; -import React, { Fragment, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Route, RouteComponentProps } from 'react-router-dom'; import classNames from 'classnames'; -import { useSelector } from 'react-redux'; import { StaticContext } from 'react-router-dom'; -import { CUSTOM_PANELS_API_PREFIX } from ' ../../../common/constants/custom_panels'; -import { uiSettingsService } from '../../../common/utils'; import { ChromeBreadcrumb, CoreStart } from '../../../../../src/core/public'; import { onTimeChange } from './helpers/utils'; import { Sidebar } from './sidebar/sidebar'; import { EmptyMetricsView } from './view/empty_view'; import PPLService from '../../services/requests/ppl'; +import { TopMenu } from './top_menu/top_menu'; +import { MetricType } from '../../../common/types/metrics'; +import { MetricsGrid } from './view/metrics_grid'; +import { useSelector } from 'react-redux'; +import { metricsLayoutSelector, selectedMetricsSelector } from './redux/slices/metrics_slice'; +import { resolutionOptions } from '../../../common/constants/metrics'; interface MetricsProps { http: CoreStart['http']; @@ -45,30 +38,71 @@ interface MetricsProps { } export const Home = ({ http, chrome, parentBreadcrumb, renderProps, pplService }: MetricsProps) => { + // Redux tools + const selectedMetrics = useSelector(selectedMetricsSelector); + const metricsLayout = useSelector(metricsLayoutSelector); + // Date picker constants const [recentlyUsedRanges, setRecentlyUsedRanges] = useState([]); - const [start, setStart] = useState('now-30m'); - const [end, setEnd] = useState('now'); - const [dateDisabled, setDateDisabled] = useState(false); + const [startTime, setStartTime] = useState('now-1d'); + const [endTime, setEndTime] = useState('now'); + + // Top panel + const [IsTopPanelDisabled, setIsTopPanelDisabled] = useState(false); + const [editMode, setEditMode] = useState(false); + const [onRefresh, setOnRefresh] = useState(false); + const [editActionType, setEditActionType] = useState(''); + const [resolutionValue, setResolutionValue] = useState(resolutionOptions[2].value); + const [spanValue, setSpanValue] = useState(1); + const resolutionSelectId = htmlIdGenerator('resolutionSelect')(); // Side bar constants const [isSidebarClosed, setIsSidebarClosed] = useState(false); - // Date Picker functions - // Empty functions for now - const onRefreshFilters = (startTime: ShortDate, endTime: ShortDate) => {}; + // Metrics constants + const [panelVisualizations, setPanelVisualizations] = useState([]); + + const onRefreshFilters = (startTime: ShortDate, endTime: ShortDate) => { + setOnRefresh(!onRefresh); + }; + const onDatePickerChange = (props: OnTimeChangeProps) => { onTimeChange( props.start, props.end, recentlyUsedRanges, setRecentlyUsedRanges, - setStart, - setEnd + setStartTime, + setEndTime ); onRefreshFilters(props.start, props.end); }; + const onEditClick = (savedVisualizationId: string) => { + window.location.assign(`#/event_analytics/explorer/${savedVisualizationId}`); + }; + + const onSideBarClick = () => { + setIsSidebarClosed((staleState) => { + return !staleState; + }); + setTimeout(function () { + window.dispatchEvent(new Event('resize')); + }, 300); + }; + + useEffect(() => { + selectedMetrics.length > 0 ? setIsTopPanelDisabled(false) : setIsTopPanelDisabled(true); + }, [selectedMetrics]); + + useEffect(() => { + setPanelVisualizations(metricsLayout); + }, [metricsLayout]); + + useEffect(() => { + if (editMode) setIsTopPanelDisabled(true); + }, [editMode]); + const mainSectionClassName = classNames({ 'col-md-9': !isSidebarClosed, 'col-md-12': isSidebarClosed, @@ -83,18 +117,23 @@ export const Home = ({ http, chrome, parentBreadcrumb, renderProps, pplService }
- - - - - +
@@ -103,11 +142,7 @@ export const Home = ({ http, chrome, parentBreadcrumb, renderProps, pplService } iconType={isSidebarClosed ? 'menuRight' : 'menuLeft'} iconSize="m" size="s" - onClick={() => { - setIsSidebarClosed((staleState) => { - return !staleState; - }); - }} + onClick={() => onSideBarClick()} data-test-subj="collapseSideBarButton" aria-controls="discover-sidebar" aria-expanded={isSidebarClosed ? 'false' : 'true'} @@ -117,11 +152,24 @@ export const Home = ({ http, chrome, parentBreadcrumb, renderProps, pplService }
-
-
- -
-
+ {selectedMetrics.length > 0 ? ( + + ) : ( + + )}
diff --git a/dashboards-observability/public/components/metrics/redux/slices/metrics_slice.ts b/dashboards-observability/public/components/metrics/redux/slices/metrics_slice.ts index cbfd2629a..fc3e038b5 100644 --- a/dashboards-observability/public/components/metrics/redux/slices/metrics_slice.ts +++ b/dashboards-observability/public/components/metrics/redux/slices/metrics_slice.ts @@ -8,13 +8,15 @@ import { PPL_PROMETHEUS_CATALOG_REQUEST, REDUX_SLICE_METRICS, } from '../../../../../common/constants/metrics'; -import { pplServiceRequestor, getVisualizations } from '../../helpers/utils'; +import { pplServiceRequestor, getVisualizations, getNewVizDimensions } from '../../helpers/utils'; import PPLService from '../../../../services/requests/ppl'; +import { MetricType } from '../../../../../common/types/metrics'; const initialState = { pplService: PPLService, metrics: [], selected: [], + metricsLayout: [], }; export const loadMetrics = createAsyncThunk('metrics/loadData', async (services: any) => { @@ -56,16 +58,57 @@ const fetchRemoteMetrics = async (pplService: any) => { return dataSet; }; +const updateLayoutBySelection = (state: any, newMetric: any) => { + const newDimensions = getNewVizDimensions(state.metricsLayout); + + const metricVisualization: MetricType = { + id: newMetric.id, + savedVisualizationId: newMetric.id, + x: newDimensions.x, + y: newDimensions.y, + h: newDimensions.h, + w: newDimensions.w, + metricType: newMetric.catalog === 'CUSTOM_METRICS' ? 'savedCustomMetric' : 'prometheusMetric', + }; + state.metricsLayout = [...state.metricsLayout, metricVisualization]; +}; + +const updateLayoutByDeSelection = (state: any, newMetric: any) => { + const sortedMetricsLayout = state.metricsLayout.sort((a: MetricType, b: MetricType) => { + if (a.y > b.y) return 1; + if (a.y < b.y) return -1; + else return 0; + }); + + let newMetricsLayout = [] as MetricType[]; + let heightSubtract = 0; + + sortedMetricsLayout.map((metricLayout: MetricType) => { + if (metricLayout.id !== newMetric.id) { + metricLayout.y = metricLayout.y - heightSubtract; + newMetricsLayout.push(metricLayout); + } else { + heightSubtract = metricLayout.h; + } + }); + state.metricsLayout = newMetricsLayout; +}; + export const metricSlice = createSlice({ name: REDUX_SLICE_METRICS, initialState, reducers: { selectMetric: (state, { payload }) => { state.selected.push(payload.id); + updateLayoutBySelection(state, payload); }, deSelectMetric: (state, { payload }) => { + updateLayoutByDeSelection(state, payload); state.selected = state.selected.filter((id) => id !== payload.id); }, + updateMetricsLayout: (state, { payload }) => { + state.metricsLayout = payload; + }, }, extraReducers: (builder) => { builder.addCase(loadMetrics.fulfilled, (state, { payload }) => { @@ -74,7 +117,7 @@ export const metricSlice = createSlice({ }, }); -export const { deSelectMetric, selectMetric } = metricSlice.actions; +export const { deSelectMetric, selectMetric, updateMetricsLayout } = metricSlice.actions; export const metricsStateSelector = (state) => state.metrics; @@ -84,4 +127,6 @@ export const availableMetricsSelector = (state) => export const selectedMetricsSelector = (state) => state.metrics.metrics.filter((metric) => state.metrics.selected.includes(metric.id)); +export const metricsLayoutSelector = (state) => state.metrics.metricsLayout; + export default metricSlice.reducer; diff --git a/dashboards-observability/public/components/metrics/top_menu/top_menu.scss b/dashboards-observability/public/components/metrics/top_menu/top_menu.scss new file mode 100644 index 000000000..8c3be93ff --- /dev/null +++ b/dashboards-observability/public/components/metrics/top_menu/top_menu.scss @@ -0,0 +1,16 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +.resolutionSelect { + .euiFormControlLayout { + .euiFormControlLayout { + width: 116px; + } + } +} + +.resolutionSelectText { + width: 100px; +} \ No newline at end of file diff --git a/dashboards-observability/public/components/metrics/top_menu/top_menu.tsx b/dashboards-observability/public/components/metrics/top_menu/top_menu.tsx new file mode 100644 index 000000000..d2aaf5492 --- /dev/null +++ b/dashboards-observability/public/components/metrics/top_menu/top_menu.tsx @@ -0,0 +1,172 @@ +import { + EuiPageHeader, + EuiPageHeaderSection, + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiFieldText, + EuiSelect, + EuiSuperDatePicker, + ShortDate, + OnTimeChangeProps, + EuiButton, +} from '@elastic/eui'; +import { DurationRange } from '@elastic/eui/src/components/date_picker/types'; +import { uiSettingsService } from '../../../../common/utils'; +import React, { useState } from 'react'; + +import './top_menu.scss'; +import { MetricType } from '../../../../common/types/metrics'; +import { resolutionOptions } from '../../../../common/constants/metrics'; + +interface TopMenuProps { + IsTopPanelDisabled: boolean; + startTime: ShortDate; + endTime: ShortDate; + onDatePickerChange: (props: OnTimeChangeProps) => void; + recentlyUsedRanges: DurationRange[]; + editMode: boolean; + setEditMode: React.Dispatch>; + setEditActionType: React.Dispatch>; + panelVisualizations: MetricType[]; + setPanelVisualizations: React.Dispatch>; + resolutionValue: string; + setResolutionValue: React.Dispatch>; + spanValue: number; + setSpanValue: React.Dispatch>; + resolutionSelectId: string; +} + +export const TopMenu = ({ + IsTopPanelDisabled, + startTime, + endTime, + onDatePickerChange, + recentlyUsedRanges, + editMode, + setEditActionType, + setEditMode, + panelVisualizations, + setPanelVisualizations, + resolutionValue, + setResolutionValue, + spanValue, + setSpanValue, + resolutionSelectId, +}: TopMenuProps) => { + const [originalPanelVisualizations, setOriginalPanelVisualizations] = useState([]); + + // toggle between panel edit mode + const editPanel = (editType: string) => { + setEditMode(!editMode); + switch (editType) { + case 'edit': { + setOriginalPanelVisualizations([...panelVisualizations]); + break; + } + case 'cancel': { + setPanelVisualizations(originalPanelVisualizations); + setOriginalPanelVisualizations([]); + break; + } + default: { + break; + } + } + setEditActionType(editType); + }; + + const onResolutionChange = (e) => { + setResolutionValue(e.target.value); + }; + + const cancelButton = ( + editPanel('cancel')}> + Cancel + + ); + + const saveButton = ( + editPanel('save')}> + Save view + + ); + + const editButton = ( + editPanel('edit')} + isDisabled={IsTopPanelDisabled} + > + Edit view + + ); + return ( + <> + + + +

Metrics

+
+
+ + + +
+ setSpanValue(e.target.value)} + append={ + onResolutionChange(e)} + aria-label="resolutionSelect" + /> + } + disabled={IsTopPanelDisabled} + aria-label="resolutionField" + /> +
+
+ + + + + {}} + data-test-subj="metrics__savePopover" + iconType="arrowDown" + isDisabled={IsTopPanelDisabled} + > + Save + + +
+
+
+ + {editMode ? ( + <> + {cancelButton} + {saveButton} + + ) : ( + {editButton} + )} + + + ); +}; diff --git a/dashboards-observability/public/components/metrics/view/empty_view.tsx b/dashboards-observability/public/components/metrics/view/empty_view.tsx index a495e2d8c..f67b6e1a4 100644 --- a/dashboards-observability/public/components/metrics/view/empty_view.tsx +++ b/dashboards-observability/public/components/metrics/view/empty_view.tsx @@ -9,17 +9,20 @@ import './empty_view.scss'; export const EmptyMetricsView = () => { return ( -
- - - - -

No Metrics Selected

- - - Select a metric from the left sidepanel to view results. - -
+
+ +
+ + + +

No Metrics Selected

+ + + Select a metric from the left sidepanel to view results. + +
+
+
); }; diff --git a/dashboards-observability/public/components/metrics/view/metrics_grid.scss b/dashboards-observability/public/components/metrics/view/metrics_grid.scss new file mode 100644 index 000000000..ff866b444 --- /dev/null +++ b/dashboards-observability/public/components/metrics/view/metrics_grid.scss @@ -0,0 +1,8 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + + .metrics-grid-div { + min-height: 70vh; + } \ No newline at end of file diff --git a/dashboards-observability/public/components/metrics/view/metrics_grid.tsx b/dashboards-observability/public/components/metrics/view/metrics_grid.tsx new file mode 100644 index 000000000..13f92d04f --- /dev/null +++ b/dashboards-observability/public/components/metrics/view/metrics_grid.tsx @@ -0,0 +1,175 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import PPLService from 'public/services/requests/ppl'; +import React, { useEffect, useState } from 'react'; +import { CoreStart } from '../../../../../../src/core/public'; +import { Layout, Layouts, Responsive, WidthProvider } from 'react-grid-layout'; +import { useObservable } from 'react-use'; +import { VisualizationContainer } from '../../custom_panels/panel_modules/visualization_container'; +import { MetricType } from '../../../../common/types/metrics'; +import _ from 'lodash'; +import { mergeLayoutAndVisualizations } from '../../custom_panels/helpers/utils'; +import { useDispatch } from 'react-redux'; +import { updateMetricsLayout, deSelectMetric } from '../redux/slices/metrics_slice'; +import { mergeLayoutAndMetrics } from '../helpers/utils'; + +import './metrics_grid.scss'; + +// HOC container to provide dynamic width for Grid layout +const ResponsiveGridLayout = WidthProvider(Responsive); + +interface MetricsGridProps { + http: CoreStart['http']; + chrome: CoreStart['chrome']; + panelVisualizations: MetricType[]; + setPanelVisualizations: React.Dispatch>; + editMode: boolean; + pplService: PPLService; + startTime: string; + endTime: string; + moveToEvents: (savedVisualizationId: string) => any; + onRefresh: boolean; + editActionType: string; + spanParam: string; +} + +export const MetricsGrid = ({ + http, + chrome, + panelVisualizations, + setPanelVisualizations, + editMode, + pplService, + startTime, + endTime, + moveToEvents, + onRefresh, + editActionType, + spanParam, +}: MetricsGridProps) => { + // Redux tools + const dispatch = useDispatch(); + const updateLayout = (metric: any) => dispatch(updateMetricsLayout(metric)); + const handleRemoveMetric = (metric: any) => { + dispatch(deSelectMetric(metric)); + }; + + const [currentLayout, setCurrentLayout] = useState([]); + const [postEditLayout, setPostEditLayout] = useState([]); + const [gridData, setGridData] = useState(panelVisualizations.map(() => <>)); + const [removeMetricsList, setRemoveMetricsList] = useState<{ id: string }[]>([]); + const isLocked = useObservable(chrome.getIsNavDrawerLocked$()); + + // Reset Size of Visualizations when layout is changed + const layoutChanged = (currLayouts: Layout[], allLayouts: Layouts) => { + window.dispatchEvent(new Event('resize')); + setPostEditLayout(currLayouts); + }; + + const loadVizComponents = () => { + const gridDataComps = panelVisualizations.map((panelVisualization: MetricType, index) => ( + + )); + setGridData(gridDataComps); + }; + + // Reload the Layout + const reloadLayout = () => { + const tempLayout: Layout[] = panelVisualizations.map((panelVisualization) => { + return { + i: panelVisualization.id, + x: panelVisualization.x, + y: panelVisualization.y, + w: panelVisualization.w, + h: panelVisualization.h, + minW: 12, // restricting width of the metric visualization + maxW: 12, + static: !editMode, + } as Layout; + }); + setCurrentLayout(tempLayout); + }; + + // remove visualization from panel in edit mode + const removeVisualization = (visualizationId: string) => { + const newVisualizationList = _.reject(panelVisualizations, { + id: visualizationId, + }); + setRemoveMetricsList([...removeMetricsList, { id: visualizationId }]); + mergeLayoutAndVisualizations(postEditLayout, newVisualizationList, setPanelVisualizations); + }; + + // Update layout whenever user edit gets completed + useEffect(() => { + if (editMode) { + reloadLayout(); + loadVizComponents(); + } + }, [editMode]); + + useEffect(() => { + if (editActionType === 'cancel') { + setRemoveMetricsList([]); + } + if (editActionType === 'save') { + removeMetricsList.map((value) => handleRemoveMetric(value)); + updateLayout(mergeLayoutAndMetrics(postEditLayout, panelVisualizations)); + } + }, [editActionType]); + + // Update layout whenever visualizations are updated + useEffect(() => { + reloadLayout(); + loadVizComponents(); + }, [panelVisualizations]); + + // Reset Size of Panel Grid when Nav Dock is Locked + useEffect(() => { + setTimeout(function () { + window.dispatchEvent(new Event('resize')); + }, 300); + }, [isLocked]); + + useEffect(() => { + loadVizComponents(); + }, [onRefresh]); + + useEffect(() => { + loadVizComponents(); + }, []); + + return ( + + {panelVisualizations.map((panelVisualization: MetricType, index) => ( +
{gridData[index]}
+ ))} +
+ ); +}; diff --git a/dashboards-observability/public/components/metrics/view/metrics_table_view.tsx b/dashboards-observability/public/components/metrics/view/metrics_table_view.tsx deleted file mode 100644 index 94c9bebd9..000000000 --- a/dashboards-observability/public/components/metrics/view/metrics_table_view.tsx +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; - -export const MetricsTableView = () => { - return
metrics_table_view
; -};