Skip to content

Commit

Permalink
adding save functionality to metrics (#1241)
Browse files Browse the repository at this point in the history
Signed-off-by: Shenoy Pratik <[email protected]>

Signed-off-by: Shenoy Pratik <[email protected]>
  • Loading branch information
ps48 authored Nov 3, 2022
1 parent 0224c23 commit 330a117
Show file tree
Hide file tree
Showing 8 changed files with 481 additions and 25 deletions.
1 change: 1 addition & 0 deletions dashboards-observability/public/components/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export const App = ({
parentBreadcrumb={parentBreadcrumb}
renderProps={props}
pplService={pplService}
savedObjects={savedObjects}
/>
);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,33 @@ export const mergeLayoutAndVisualizations = (
setPanelVisualizations(newPanelVisualizations);
};

/* Update Span interval for a Query
* Input query -> source = opensearch_dashboards_sample_data_logs | stats avg(bytes) by span(timestamp,1d)
* spanParam -> 1M
*
* Updates the span command interval
* Returns -> source = opensearch_dashboards_sample_data_logs | stats avg(bytes) by span(timestamp,1M)
*/
export const updateQuerySpanInterval = (
query: string,
timestampField: string,
spanParam: string
) => {
return query.replace(
new RegExp(`span\\((.*?)${timestampField}(.*?),(.*?)\\)`),
`span(${timestampField},${spanParam})`
);
};

/* 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 > ‘2021-07-01 00:00:00’ and utc_time < ‘2021-07-02 00:00:00’
* + | where Carrier='OpenSearch-Air'
* + | stats sum(FlightDelayMin) as delays by Carrier
*
* Also, checks is span interval update is needed and retruns accordingly
*/
const queryAccumulator = (
originalQuery: string,
Expand All @@ -103,15 +123,12 @@ const queryAccumulator = (
startTime
)}' and ${timestampField} <= '${convertDateTime(endTime, false)}'`;
const pplFilterQuery = panelFilterQuery === '' ? '' : ` | ${panelFilterQuery}`;

const finalQuery = indexPartOfQuery + timeQueryFilter + pplFilterQuery + filterPartOfQuery;
if (spanParam === undefined) {
return finalQuery;
} else {
return finalQuery.replace(
new RegExp(`span\\(${timestampField},(.*?)\\)`),
`span(${timestampField},${spanParam})`
);
}

return spanParam === undefined
? finalQuery
: updateQuerySpanInterval(finalQuery, timestampField, spanParam);
};

// PPL Service requestor
Expand Down Expand Up @@ -400,7 +417,7 @@ export const displayVisualization = (metaData: any, data: any, type: string) =>
),
},
};

return (
<Visualization
visualizations={getVizContainerProps({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { CoreStart } from '../../../../../../src/core/public';
import { MetricType } from '../../../../common/types/metrics';
import { VisualizationType } from '../../../../common/types/custom_panels';
import { DEFAULT_METRIC_HEIGHT, DEFAULT_METRIC_WIDTH } from '../../../../common/constants/metrics';
import { UNITS_OF_MEASURE } from '../../../../common/constants/explorer';
import { updateQuerySpanInterval } from '../../custom_panels/helpers/utils';

export const onTimeChange = (
start: ShortDate,
Expand Down Expand Up @@ -161,3 +163,52 @@ export const mergeLayoutAndMetrics = (
}
return newPanelVisualizations;
};

export const sortMetricLayout = (metricsLayout: MetricType[]) => {
return metricsLayout.sort((a: MetricType, b: MetricType) => {
if (a.y > b.y) return 1;
if (a.y < b.y) return -1;
else return 0;
});
};

export const createPrometheusMetricById = (metricId: string) => {
return {
name: '[Prometheus Metric] ' + metricId,
description: '',
query: 'source = ' + metricId + ' | stats avg(@value) by span(@timestamp,1h)',
type: 'line',
timeField: '@timestamp',
selected_fields: {
text: '',
tokens: [],
},
sub_type: 'metric',
units_of_measure: UNITS_OF_MEASURE[1],
user_configs: {},
};
};

export const updateMetricsWithSelections = (
savedVisualization: any,
startTime: ShortDate,
endTime: ShortDate,
spanValue: string
) => {
return {
query: updateQuerySpanInterval(
savedVisualization.query,
savedVisualization.timeField,
spanValue
),
fields: savedVisualization.selected_fields.tokens,
dateRange: [startTime, endTime],
timestamp: savedVisualization.timeField,
name: savedVisualization.name,
description: savedVisualization.description,
type: 'line',
subType: 'metric',
userConfigs: JSON.stringify(savedVisualization.user_configs),
unitsOfMeasure: savedVisualization.units_of_measure,
};
};
33 changes: 31 additions & 2 deletions dashboards-observability/public/components/metrics/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import './index.scss';
import {
EuiButtonIcon,
EuiGlobalToastList,
EuiPage,
EuiPageBody,
htmlIdGenerator,
Expand All @@ -17,7 +18,7 @@ import React, { useEffect, useState } from 'react';
import { Route, RouteComponentProps } from 'react-router-dom';
import classNames from 'classnames';
import { StaticContext } from 'react-router-dom';
import { ChromeBreadcrumb, CoreStart } from '../../../../../src/core/public';
import { ChromeBreadcrumb, CoreStart, Toast } from '../../../../../src/core/public';
import { onTimeChange } from './helpers/utils';
import { Sidebar } from './sidebar/sidebar';
import { EmptyMetricsView } from './view/empty_view';
Expand All @@ -28,16 +29,25 @@ 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';
import SavedObjects from '../../services/saved_objects/event_analytics/saved_objects';

interface MetricsProps {
http: CoreStart['http'];
chrome: CoreStart['chrome'];
parentBreadcrumb: ChromeBreadcrumb;
renderProps: RouteComponentProps<any, StaticContext, any>;
pplService: PPLService;
savedObjects: SavedObjects;
}

export const Home = ({ http, chrome, parentBreadcrumb, renderProps, pplService }: MetricsProps) => {
export const Home = ({
http,
chrome,
parentBreadcrumb,
renderProps,
pplService,
savedObjects,
}: MetricsProps) => {
// Redux tools
const selectedMetrics = useSelector(selectedMetricsSelector);
const metricsLayout = useSelector(metricsLayoutSelector);
Expand All @@ -55,13 +65,21 @@ export const Home = ({ http, chrome, parentBreadcrumb, renderProps, pplService }
const [resolutionValue, setResolutionValue] = useState(resolutionOptions[2].value);
const [spanValue, setSpanValue] = useState(1);
const resolutionSelectId = htmlIdGenerator('resolutionSelect')();
const [toasts, setToasts] = useState<Toast[]>([]);
const [toastRightSide, setToastRightSide] = useState<boolean>(true);

// Side bar constants
const [isSidebarClosed, setIsSidebarClosed] = useState(false);

// Metrics constants
const [panelVisualizations, setPanelVisualizations] = useState<MetricType[]>([]);

const setToast = (title: string, color = 'success', text?: ReactChild, side?: string) => {
if (!text) text = '';
setToastRightSide(!side ? true : false);
setToasts([...toasts, { id: new Date().toISOString(), title, text, color } as Toast]);
};

const onRefreshFilters = (startTime: ShortDate, endTime: ShortDate) => {
setOnRefresh(!onRefresh);
};
Expand Down Expand Up @@ -114,6 +132,14 @@ export const Home = ({ http, chrome, parentBreadcrumb, renderProps, pplService }

return (
<>
<EuiGlobalToastList
toasts={toasts}
dismissToast={(removedToast) => {
setToasts(toasts.filter((toast) => toast.id !== removedToast.id));
}}
side={toastRightSide ? 'right' : 'left'}
toastLifeTimeMs={6000}
/>
<Route
exact
path="/metrics"
Expand All @@ -122,6 +148,7 @@ export const Home = ({ http, chrome, parentBreadcrumb, renderProps, pplService }
<EuiPage>
<EuiPageBody component="div">
<TopMenu
http={http}
IsTopPanelDisabled={IsTopPanelDisabled}
startTime={startTime}
endTime={endTime}
Expand All @@ -137,6 +164,8 @@ export const Home = ({ http, chrome, parentBreadcrumb, renderProps, pplService }
spanValue={spanValue}
setSpanValue={setSpanValue}
resolutionSelectId={resolutionSelectId}
savedObjects={savedObjects}
setToast={setToast}
/>
<div className="dscAppContainer">
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { CUSTOM_PANELS_API_PREFIX } from '../../../../common/constants/custom_panels';
import React, { useEffect, useState } from 'react';
import { CoreStart } from '../../../../../../src/core/public';
import {
EuiComboBox,
EuiComboBoxOptionOption,
EuiFieldText,
EuiFlexGroup,
EuiFormRow,
EuiFlexItem,
EuiForm,
EuiSelect,
} from '@elastic/eui';
import { UNITS_OF_MEASURE } from '../../../../common/constants/explorer';
import { createPrometheusMetricById } from '../helpers/utils';
import { MetricType } from '../../../../common/types/metrics';
import { fetchVisualizationById } from '../../custom_panels/helpers/utils';

interface MetricsExportPanelProps {
http: CoreStart['http'];
visualizationsMetaData: any;
setVisualizationsMetaData: React.Dispatch<any>;
sortedMetricsLayout: MetricType[];
selectedPanelOptions: EuiComboBoxOptionOption<unknown>[] | undefined;
setSelectedPanelOptions: React.Dispatch<
React.SetStateAction<EuiComboBoxOptionOption<unknown>[] | undefined>
>;
}

interface CustomPanelOptions {
id: string;
name: string;
dateCreated: string;
dateModified: string;
}

export const MetricsExportPanel = ({
http,
visualizationsMetaData,
setVisualizationsMetaData,
sortedMetricsLayout,
selectedPanelOptions,
setSelectedPanelOptions,
}: MetricsExportPanelProps) => {
const [options, setOptions] = useState([]);

const [errorResponse, setErrorResponse] = useState('');

const getCustomPanelList = async () => {
http
.get(`${CUSTOM_PANELS_API_PREFIX}/panels`)
.then((res: any) => {
setOptions(res.panels || []);
})
.catch((error: any) => console.error(error));
};

const fetchAllvisualizationsById = async () => {
let tempVisualizationsMetaData = await Promise.all(
sortedMetricsLayout.map(async (metricLayout) => {
return metricLayout.metricType === 'savedCustomMetric'
? await fetchVisualizationById(http, metricLayout.id, setErrorResponse)
: createPrometheusMetricById(metricLayout.id);
})
);
console.log('tempVisualizationsMetaData', tempVisualizationsMetaData);
setVisualizationsMetaData(tempVisualizationsMetaData);
};

useEffect(() => {
getCustomPanelList();
fetchAllvisualizationsById();
}, []);

const onNameChange = (index: number, name: string) => {
let tempVisualizationsMetaData = [...visualizationsMetaData];
tempVisualizationsMetaData[index].name = name;
setVisualizationsMetaData(tempVisualizationsMetaData);
};

const onMeasureChange = (index: number, measureOption: any) => {
let tempVisualizationsMetaData = [...visualizationsMetaData];
tempVisualizationsMetaData[index].units_of_measure = measureOption;
setVisualizationsMetaData(tempVisualizationsMetaData);
};

return (
<div style={{ minWidth: '25vw' }}>
<EuiFormRow
label="Custom operational dashboards/application"
helpText="Search existing dashboards or applications by name"
>
<EuiComboBox
placeholder="Select dashboards/applications"
onChange={(options) => {
setSelectedPanelOptions(options);
}}
selectedOptions={selectedPanelOptions}
options={options.map((option: CustomPanelOptions) => {
return {
panel: option,
label: option.name,
};
})}
isClearable={true}
data-test-subj="eventExplorer__querySaveComboBox"
/>
</EuiFormRow>

{visualizationsMetaData.length > 0 && (
<div style={{ maxHeight: '30vh', overflowY: 'scroll', width: 'auto', overflowX: 'hidden' }}>
{visualizationsMetaData.map((metaData: any, index: number) => {
return (
<EuiForm component="form">
<EuiFlexGroup>
<EuiFlexItem>
<EuiFormRow label={'Metric Name #' + (index + 1)}>
<EuiFieldText
key={'save-panel-id'}
value={visualizationsMetaData[index].name}
onChange={(e) => onNameChange(index, e.target.value)}
data-test-subj="metrics__querySaveName"
/>
</EuiFormRow>
</EuiFlexItem>

<EuiFlexItem>
<EuiFormRow label="Units of Measure">
<EuiSelect
id={'selector' + index}
options={UNITS_OF_MEASURE.map((i) => {
return { value: i, text: i };
})}
value={visualizationsMetaData[index].units_of_measure}
onChange={(e) => onMeasureChange(index, e.target.value)}
data-test-subj="metrics__measureSelector"
aria-label="metrics__measureSelector"
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
</EuiForm>
);
})}
</div>
)}
</div>
);
};
Loading

0 comments on commit 330a117

Please sign in to comment.