Merge pull request #3228 from SigNoz/release/v0.25.0

Release/v0.25.0
This commit is contained in:
Srikanth Chekuri 2023-07-29 16:32:11 +05:30 committed by GitHub
commit 7493193e66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
133 changed files with 2493 additions and 1104 deletions

View File

@ -137,7 +137,7 @@ services:
condition: on-failure
query-service:
image: signoz/query-service:0.24.0
image: signoz/query-service:0.25.0
command: ["-config=/root/config/prometheus.yml"]
# ports:
# - "6060:6060" # pprof port
@ -166,7 +166,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:0.24.0
image: signoz/frontend:0.25.0
deploy:
restart_policy:
condition: on-failure

View File

@ -153,7 +153,7 @@ services:
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
query-service:
image: signoz/query-service:${DOCKER_TAG:-0.24.0}
image: signoz/query-service:${DOCKER_TAG:-0.25.0}
container_name: query-service
command: ["-config=/root/config/prometheus.yml"]
# ports:
@ -181,7 +181,7 @@ services:
<<: *clickhouse-depend
frontend:
image: signoz/frontend:${DOCKER_TAG:-0.24.0}
image: signoz/frontend:${DOCKER_TAG:-0.25.0}
container_name: frontend
restart: on-failure
depends_on:

View File

@ -1,7 +1,7 @@
import Loadable from 'components/Loadable';
export const ServicesTablePage = Loadable(
() => import(/* webpackChunkName: "ServicesTablePage" */ 'pages/Metrics'),
() => import(/* webpackChunkName: "ServicesTablePage" */ 'pages/Services'),
);
export const ServiceMetricsPage = Loadable(

View File

@ -1,28 +1,13 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/metrics/getService';
const getService = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const response = await axios.post(`/services`, {
start: `${props.start}`,
end: `${props.end}`,
tags: props.selectedTags,
});
return {
statusCode: 200,
error: null,
message: response.data.status,
payload: response.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
const getService = async (props: Props): Promise<PayloadProps> => {
const response = await axios.post(`/services`, {
start: `${props.start}`,
end: `${props.end}`,
tags: props.selectedTags,
});
return response.data;
};
export default getService;

View File

@ -0,0 +1,18 @@
function Table(): JSX.Element {
return (
<svg
width="48"
height="48"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M41.0667 0H6.39993C2.87982 0 0 2.87982 0 6.39993V41.6001C0 45.1202 2.87982 48 6.39993 48H41.0667C44.5868 48 47.4667 45.1202 47.4667 41.6001V6.39993C47.4667 2.87982 44.5868 0 41.0667 0ZM44.2669 6.39993V9.60013H32.0002V3.2002H41.0668C42.8268 3.2002 44.267 4.63992 44.267 6.40003L44.2669 6.39993ZM17.6002 9.60013V3.2002H29.8669V9.60013H17.6002ZM29.8669 11.7333V44.8001H17.6002L17.6005 11.7333H29.8669ZM6.40012 3.20011H15.4667V9.60004H3.20001V6.39984C3.20001 4.63983 4.64011 3.20001 6.40022 3.20001L6.40012 3.20011ZM3.19992 41.6003V11.7335H15.4666V44.8003H6.40003C4.64002 44.8003 3.19982 43.3606 3.19982 41.6005L3.19992 41.6003ZM41.0667 44.8001H32.0001V11.7333H44.2668V41.6001C44.2668 43.3601 42.8267 44.7999 41.0666 44.7999L41.0667 44.8001ZM5.33326 18.6666C5.33326 18.08 5.81317 17.6001 6.39983 17.6001H12.2667C12.8534 17.6001 13.3333 18.08 13.3333 18.6666C13.3333 19.2533 12.8534 19.7332 12.2667 19.7332H6.39983C5.81315 19.7332 5.33326 19.2533 5.33326 18.6666ZM13.3333 25.0666C13.3333 25.6533 12.8534 26.1332 12.2667 26.1332H6.39983C5.81315 26.1332 5.33326 25.6532 5.33326 25.0666C5.33326 24.4799 5.81317 24 6.39983 24H12.2667C12.8534 24 13.3333 24.4799 13.3333 25.0666ZM13.3333 31.4665C13.3333 32.0532 12.8534 32.5331 12.2667 32.5331H6.39983C5.81315 32.5331 5.33326 32.0532 5.33326 31.4665C5.33326 30.8798 5.81317 30.3999 6.39983 30.3999H12.2667C12.8534 30.3999 13.3333 30.8798 13.3333 31.4665ZM13.3333 37.8668C13.3333 38.4535 12.8534 38.9334 12.2667 38.9334H6.39983C5.81315 38.9334 5.33326 38.4535 5.33326 37.8668C5.33326 37.2801 5.81317 36.8002 6.39983 36.8002H12.2667C12.8534 36.7999 13.3333 37.2802 13.3333 37.8668ZM19.7332 18.6667C19.7332 18.0801 20.2131 17.6002 20.7998 17.6002H26.6667C27.2534 17.6002 27.7333 18.0801 27.7333 18.6667C27.7333 19.2534 27.2533 19.7333 26.6667 19.7333H20.7998C20.2131 19.7333 19.7332 19.2534 19.7332 18.6667ZM19.7332 25.0667C19.7332 24.48 20.2131 24.0001 20.7998 24.0001H26.6667C27.2534 24.0001 27.7333 24.48 27.7333 25.0667C27.7333 25.6534 27.2533 26.1332 26.6667 26.1332H20.7998C20.2131 26.1332 19.7332 25.6533 19.7332 25.0667ZM19.7332 31.4666C19.7332 30.8799 20.2131 30.4 20.7998 30.4H26.6667C27.2534 30.4 27.7333 30.8799 27.7333 31.4666C27.7333 32.0533 27.2533 32.5332 26.6667 32.5332H20.7998C20.2131 32.5336 19.7332 32.0533 19.7332 31.4666ZM27.7333 37.8669C27.7333 38.4536 27.2533 38.9335 26.6667 38.9335H20.7998C20.2131 38.9335 19.7332 38.4536 19.7332 37.8669C19.7332 37.2802 20.2131 36.8003 20.7998 36.8003H26.6667C27.2534 36.8 27.7333 37.2803 27.7333 37.8669ZM42.1333 18.6668C42.1333 19.2535 41.6534 19.7334 41.0667 19.7334H35.1999C34.6132 19.7334 34.1333 19.2535 34.1333 18.6668C34.1333 18.0802 34.6132 17.6003 35.1999 17.6003H41.0667C41.6534 17.6003 42.1333 18.0802 42.1333 18.6668ZM42.1333 25.0668C42.1333 25.6535 41.6534 26.1333 41.0667 26.1333H35.1999C34.6132 26.1333 34.1333 25.6534 34.1333 25.0668C34.1333 24.4801 34.6132 24.0002 35.1999 24.0002H41.0667C41.6534 24.0002 42.1333 24.4801 42.1333 25.0668ZM42.1333 31.4667C42.1333 32.0534 41.6534 32.5333 41.0667 32.5333H35.1999C34.6132 32.5333 34.1333 32.0534 34.1333 31.4667C34.1333 30.88 34.6132 30.4001 35.1999 30.4001H41.0667C41.6534 30.4001 42.1333 30.88 42.1333 31.4667ZM42.1333 37.867C42.1333 38.4537 41.6534 38.9336 41.0667 38.9336H35.1999C34.6132 38.9336 34.1333 38.4537 34.1333 37.867C34.1333 37.2803 34.6132 36.8004 35.1999 36.8004H41.0667C41.6534 36.8001 42.1333 37.2803 42.1333 37.867Z"
fill="#1668DC"
/>
</svg>
);
}
export default Table;

File diff suppressed because one or more lines are too long

View File

@ -1,26 +1,22 @@
import { CSSProperties } from 'react';
function Value(props: ValueProps): JSX.Element {
const { fillColor } = props;
function Value(): JSX.Element {
return (
<svg
width="78"
height="32"
viewBox="0 0 78 32"
width="68"
height="48"
viewBox="0 0 68 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M15.0215 17.875C14.2285 18.8184 13.2783 19.5771 12.1709 20.1514C11.0771 20.7256 9.87402 21.0127 8.56152 21.0127C6.83887 21.0127 5.33496 20.5889 4.0498 19.7412C2.77832 18.8936 1.79395 17.7041 1.09668 16.1729C0.399414 14.6279 0.0507812 12.9258 0.0507812 11.0664C0.0507812 9.07031 0.426758 7.27246 1.17871 5.67285C1.94434 4.07324 3.02441 2.84961 4.41895 2.00195C5.81348 1.1543 7.44043 0.730469 9.2998 0.730469C12.2529 0.730469 14.5771 1.83789 16.2725 4.05273C17.9814 6.25391 18.8359 9.26172 18.8359 13.0762V14.1836C18.8359 19.9941 17.6875 24.2393 15.3906 26.9189C13.0938 29.585 9.62793 30.9521 4.99316 31.0205H4.25488V27.8213H5.05469C8.18555 27.7666 10.5918 26.9531 12.2734 25.3809C13.9551 23.7949 14.8711 21.293 15.0215 17.875ZM9.17676 17.875C10.4482 17.875 11.6172 17.4854 12.6836 16.7061C13.7637 15.9268 14.5498 14.9629 15.042 13.8145V12.2969C15.042 9.80859 14.502 7.78516 13.4219 6.22656C12.3418 4.66797 10.9746 3.88867 9.32031 3.88867C7.65234 3.88867 6.3125 4.53125 5.30078 5.81641C4.28906 7.08789 3.7832 8.76953 3.7832 10.8613C3.7832 12.8984 4.26855 14.5801 5.23926 15.9062C6.22363 17.2188 7.53613 17.875 9.17676 17.875ZM24.5371 29.0107C24.5371 28.3545 24.7285 27.8076 25.1113 27.3701C25.5078 26.9326 26.0957 26.7139 26.875 26.7139C27.6543 26.7139 28.2422 26.9326 28.6387 27.3701C29.0488 27.8076 29.2539 28.3545 29.2539 29.0107C29.2539 29.6396 29.0488 30.166 28.6387 30.5898C28.2422 31.0137 27.6543 31.2256 26.875 31.2256C26.0957 31.2256 25.5078 31.0137 25.1113 30.5898C24.7285 30.166 24.5371 29.6396 24.5371 29.0107ZM51.1562 20.9717H55.2988V24.0684H51.1562V31H47.3418V24.0684H33.7451V21.833L47.1162 1.14062H51.1562V20.9717ZM38.0518 20.9717H47.3418V6.3291L46.8906 7.14941L38.0518 20.9717ZM73.6123 1.12012V4.33984H72.915C69.9619 4.39453 67.6104 5.26953 65.8604 6.96484C64.1104 8.66016 63.0986 11.0459 62.8252 14.1221C64.3975 12.3174 66.5439 11.415 69.2646 11.415C71.8623 11.415 73.9336 12.3311 75.4785 14.1631C77.0371 15.9951 77.8164 18.3604 77.8164 21.2588C77.8164 24.335 76.9756 26.7959 75.2939 28.6416C73.626 30.4873 71.3838 31.4102 68.5674 31.4102C65.71 31.4102 63.3926 30.3164 61.6152 28.1289C59.8379 25.9277 58.9492 23.0977 58.9492 19.6387V18.1826C58.9492 12.6865 60.1182 8.48926 62.4561 5.59082C64.8076 2.67871 68.3008 1.18848 72.9355 1.12012H73.6123ZM68.6289 14.5732C67.3301 14.5732 66.1338 14.9629 65.04 15.7422C63.9463 16.5215 63.1875 17.499 62.7637 18.6748V20.0693C62.7637 22.5303 63.3174 24.5127 64.4248 26.0166C65.5322 27.5205 66.9131 28.2725 68.5674 28.2725C70.2764 28.2725 71.6162 27.6436 72.5869 26.3857C73.5713 25.1279 74.0635 23.4805 74.0635 21.4434C74.0635 19.3926 73.5645 17.7383 72.5664 16.4805C71.582 15.209 70.2695 14.5732 68.6289 14.5732Z"
fill={fillColor}
d="M0 5.914V42.086C0 43.6542 0.62289 45.1585 1.73183 46.2675C2.84078 47.3771 4.34511 48 5.91329 48H61.5019C63.0701 48 64.5744 47.3771 65.6834 46.2675C66.7923 45.1585 67.4152 43.6542 67.4152 42.086V5.914C67.4152 4.34578 66.7923 2.84152 65.6834 1.73253C64.5744 0.623576 63.0701 0 61.5019 0H5.91329C4.34508 0 2.84082 0.623576 1.73183 1.73253C0.622872 2.84149 0 4.34581 0 5.914ZM63.4735 5.914V42.086C63.4735 42.6092 63.2659 43.1104 62.896 43.4803C62.5261 43.8495 62.0249 44.0571 61.5024 44.0571H5.91382C4.82549 44.0571 3.94277 43.175 3.94277 42.086V5.91403C3.94277 4.8257 4.82553 3.94298 5.91382 3.94298H61.5024C62.0249 3.94298 62.5261 4.15061 62.896 4.52048C63.2659 4.88968 63.4735 5.39148 63.4735 5.914Z"
fill="#1554AD"
/>
<path
d="M13.7695 17.668C10.1016 17.668 7.48828 20.1758 7.48828 23.6094V23.6328C7.48828 26.8438 9.76172 29.2109 13.0078 29.2109C15.3281 29.2109 16.8047 28.0273 17.4258 26.6914H17.6602C17.6602 26.8203 17.6484 26.9492 17.6484 27.0781C17.5195 30.3125 16.3828 32.9375 13.6992 32.9375C12.2109 32.9375 11.168 32.1641 10.7227 30.9805L10.6875 30.8633H7.71094L7.73438 30.9922C8.27344 33.582 10.5938 35.4219 13.6992 35.4219C17.9531 35.4219 20.5195 32.0469 20.5195 26.3516V26.3281C20.5195 20.2344 17.3789 17.668 13.7695 17.668ZM13.7578 26.8906C11.8359 26.8906 10.4414 25.4844 10.4414 23.5273V23.5039C10.4414 21.6172 11.9297 20.1289 13.793 20.1289C15.668 20.1289 17.1328 21.6406 17.1328 23.5742V23.5977C17.1328 25.5078 15.668 26.8906 13.7578 26.8906ZM24.832 35.2344C25.9102 35.2344 26.6953 34.4258 26.6953 33.3828C26.6953 32.3398 25.9102 31.5312 24.832 31.5312C23.7656 31.5312 22.9688 32.3398 22.9688 33.3828C22.9688 34.4258 23.7656 35.2344 24.832 35.2344ZM37.8633 35H40.7578V31.7539H43.0312V29.2578H40.7578V18.0898H36.4805C34.1836 21.582 31.7812 25.4727 29.5898 29.2812V31.7539H37.8633V35ZM32.4023 29.3281V29.1523C34.043 26.2812 36 23.1523 37.7344 20.5039H37.9102V29.3281H32.4023ZM52.6406 35.4219C56.3086 35.4219 58.9219 32.9141 58.9219 29.4805V29.457C58.9219 26.2461 56.6484 23.8789 53.4023 23.8789C51.082 23.8789 49.6055 25.0625 48.9844 26.3984H48.75C48.75 26.2695 48.7617 26.1406 48.7617 26.0117C48.8906 22.7773 50.0273 20.1523 52.7109 20.1523C54.1992 20.1523 55.2422 20.9258 55.6875 22.1094L55.7344 22.2266H58.6992L58.6758 22.0977C58.1367 19.5078 55.8164 17.668 52.7109 17.668C48.457 17.668 45.8906 21.043 45.8906 26.7383V26.7617C45.8906 32.8555 49.0312 35.4219 52.6406 35.4219ZM49.2773 29.5156V29.4922C49.2773 27.582 50.7422 26.1992 52.6523 26.1992C54.5742 26.1992 55.9688 27.6055 55.9688 29.5625V29.5859C55.9688 31.4727 54.4922 32.9609 52.6172 32.9609C50.7422 32.9609 49.2773 31.4492 49.2773 29.5156Z"
fill="#1668DC"
/>
</svg>
);
}
interface ValueProps {
fillColor: CSSProperties['color'];
}
export default Value;

View File

@ -19,6 +19,7 @@ const getOrCreateLegendList = (
listContainer.style.overflowY = 'scroll';
listContainer.style.justifyContent = isLonger ? 'start' : 'center';
listContainer.style.alignItems = isLonger ? 'start' : 'center';
listContainer.style.minHeight = '2rem';
listContainer.style.height = '100%';
listContainer.style.flexWrap = 'wrap';
listContainer.style.justifyContent = 'center';

View File

@ -343,7 +343,7 @@ type CustomChartOptions = ChartOptions & {
};
};
interface GraphProps {
export interface GraphProps {
animate?: boolean;
type: ChartType;
data: Chart['data'];

View File

@ -0,0 +1,14 @@
import Graph from 'components/Graph';
import GridTableComponent from 'container/GridTableComponent';
import GridValueComponent from 'container/GridValueComponent';
import { PANEL_TYPES } from './queryBuilder';
export const PANEL_TYPES_COMPONENT_MAP = {
[PANEL_TYPES.TIME_SERIES]: Graph,
[PANEL_TYPES.VALUE]: GridValueComponent,
[PANEL_TYPES.TABLE]: GridTableComponent,
[PANEL_TYPES.TRACE]: null,
[PANEL_TYPES.LIST]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null,
} as const;

View File

@ -1,5 +1,4 @@
// ** Helpers
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
import {
@ -24,7 +23,6 @@ import {
LogsAggregatorOperator,
MetricAggregateOperator,
NumberOperators,
PanelTypeKeys,
QueryAdditionalFilter,
QueryBuilderData,
ReduceOperators,
@ -124,10 +122,10 @@ export const initialFilters: TagFilter = {
op: 'AND',
};
const initialQueryBuilderFormValues: IBuilderQuery = {
export const initialQueryBuilderFormValues: IBuilderQuery = {
dataSource: DataSource.METRICS,
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
aggregateOperator: MetricAggregateOperator.NOOP,
aggregateOperator: MetricAggregateOperator.COUNT,
aggregateAttribute: initialAutocompleteData,
filters: { items: [], op: 'AND' },
expression: createNewBuilderItemName({
@ -238,14 +236,15 @@ export const operatorsByTypes: Record<LocalDataType, string[]> = {
bool: Object.values(BoolOperators),
};
export const PANEL_TYPES: Record<PanelTypeKeys, GRAPH_TYPES> = {
TIME_SERIES: 'graph',
VALUE: 'value',
TABLE: 'table',
LIST: 'list',
TRACE: 'trace',
EMPTY_WIDGET: 'EMPTY_WIDGET',
};
// eslint-disable-next-line @typescript-eslint/naming-convention
export enum PANEL_TYPES {
TIME_SERIES = 'graph',
VALUE = 'value',
TABLE = 'table',
LIST = 'list',
TRACE = 'trace',
EMPTY_WIDGET = 'EMPTY_WIDGET',
}
export type IQueryBuilderState = 'search';

View File

@ -2,8 +2,7 @@ import { InfoCircleOutlined } from '@ant-design/icons';
import { StaticLineProps } from 'components/Graph';
import Spinner from 'components/Spinner';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import GridGraphComponent from 'container/GridGraphComponent';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
@ -18,7 +17,7 @@ import { ChartContainer, FailedMessageContainer } from './styles';
export interface ChartPreviewProps {
name: string;
query: Query | null;
graphType?: GRAPH_TYPES;
graphType?: PANEL_TYPES;
selectedTime?: timePreferenceType;
selectedInterval?: Time;
headline?: JSX.Element;
@ -113,13 +112,15 @@ function ChartPreview({
<Spinner size="large" tip="Loading..." height="70vh" />
)}
{chartDataSet && !queryResponse.isError && (
<GridGraphComponent
<GridPanelSwitch
panelType={graphType}
title={name}
data={chartDataSet}
isStacked
GRAPH_TYPES={graphType || PANEL_TYPES.TIME_SERIES}
name={name || 'Chart Preview'}
staticLine={staticLine}
panelData={queryResponse.data?.payload.data.newResult.data.result || []}
query={query || initialQueriesMap.metrics}
/>
)}
</ChartContainer>

View File

@ -3,6 +3,7 @@ import { Col, FormInstance, Modal, Tooltip, Typography } from 'antd';
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import { FeatureKeys } from 'constants/features';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
@ -58,6 +59,7 @@ function FormAlertRules({
const {
currentQuery,
panelType,
stagedQuery,
handleRunQuery,
redirectWithQueryBuilderData,
@ -351,7 +353,12 @@ function FormAlertRules({
const renderQBChartPreview = (): JSX.Element => (
<ChartPreview
headline={<PlotTag queryType={currentQuery.queryType} />}
headline={
<PlotTag
queryType={currentQuery.queryType}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
/>
}
name=""
threshold={alertDef.condition?.target}
query={stagedQuery}
@ -361,7 +368,12 @@ function FormAlertRules({
const renderPromChartPreview = (): JSX.Element => (
<ChartPreview
headline={<PlotTag queryType={currentQuery.queryType} />}
headline={
<PlotTag
queryType={currentQuery.queryType}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
/>
}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={stagedQuery}
@ -370,7 +382,12 @@ function FormAlertRules({
const renderChQueryChartPreview = (): JSX.Element => (
<ChartPreview
headline={<PlotTag queryType={currentQuery.queryType} />}
headline={
<PlotTag
queryType={currentQuery.queryType}
panelType={panelType || PANEL_TYPES.TIME_SERIES}
/>
}
name="Chart Preview"
threshold={alertDef.condition?.target}
query={stagedQuery}

View File

@ -1,103 +0,0 @@
import { Typography } from 'antd';
import { ChartData } from 'chart.js';
import Graph, { GraphOnClickHandler, StaticLineProps } from 'components/Graph';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import ValueGraph from 'components/ValueGraph';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import history from 'lib/history';
import { TitleContainer, ValueContainer } from './styles';
function GridGraphComponent({
GRAPH_TYPES,
data,
title,
opacity,
isStacked,
onClickHandler,
name,
yAxisUnit,
staticLine,
onDragSelect,
}: GridGraphComponentProps): JSX.Element | null {
const location = history.location.pathname;
const isDashboardPage = location.split('/').length === 3;
if (GRAPH_TYPES === PANEL_TYPES.TIME_SERIES) {
return (
<Graph
{...{
data,
title,
type: 'line',
isStacked,
opacity,
xAxisType: 'time',
onClickHandler,
name,
yAxisUnit,
staticLine,
onDragSelect,
}}
/>
);
}
if (GRAPH_TYPES === PANEL_TYPES.VALUE) {
const value = (((data.datasets[0] || []).data || [])[0] || 0) as number;
if (data.datasets.length === 0) {
return (
<ValueContainer isDashboardPage={isDashboardPage}>
<Typography>No Data</Typography>
</ValueContainer>
);
}
return (
<>
<TitleContainer isDashboardPage={isDashboardPage}>
<Typography>{title}</Typography>
</TitleContainer>
<ValueContainer isDashboardPage={isDashboardPage}>
<ValueGraph
value={
yAxisUnit
? getYAxisFormattedValue(String(value), yAxisUnit)
: value.toString()
}
/>
</ValueContainer>
</>
);
}
return null;
}
export interface GridGraphComponentProps {
GRAPH_TYPES: GRAPH_TYPES;
data: ChartData;
title?: string;
opacity?: string;
isStacked?: boolean;
onClickHandler?: GraphOnClickHandler;
name: string;
yAxisUnit?: string;
staticLine?: StaticLineProps;
onDragSelect?: (start: number, end: number) => void;
}
GridGraphComponent.defaultProps = {
title: undefined,
opacity: undefined,
isStacked: undefined,
onClickHandler: undefined,
yAxisUnit: undefined,
staticLine: undefined,
onDragSelect: undefined,
};
export default GridGraphComponent;

View File

@ -2,7 +2,7 @@ import { Button } from 'antd';
import { GraphOnClickHandler } from 'components/Graph';
import Spinner from 'components/Spinner';
import TimePreference from 'components/TimePreferenceDropDown';
import GridGraphComponent from 'container/GridGraphComponent';
import GridPanelSwitch from 'container/GridPanelSwitch';
import {
timeItems,
timePreferance,
@ -85,7 +85,7 @@ function FullView({
return (
<>
{fullViewOptions && (
<TimeContainer>
<TimeContainer $panelType={widget.panelTypes}>
<TimePreference
selectedTime={selectedTime}
setSelectedTime={setSelectedTime}
@ -101,8 +101,8 @@ function FullView({
</TimeContainer>
)}
<GridGraphComponent
GRAPH_TYPES={widget.panelTypes}
<GridPanelSwitch
panelType={widget.panelTypes}
data={chartDataSet}
isStacked={widget.isStacked}
opacity={widget.opacity}
@ -111,6 +111,8 @@ function FullView({
name={name}
yAxisUnit={yAxisUnit}
onDragSelect={onDragSelect}
panelData={response.data?.payload.data.newResult.data.result || []}
query={widget.query}
/>
</>
);

View File

@ -1,4 +1,9 @@
import styled from 'styled-components';
import { PANEL_TYPES } from 'constants/queryBuilder';
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
interface Props {
$panelType: PANEL_TYPES;
}
export const NotFoundContainer = styled.div`
display: flex;
@ -7,7 +12,13 @@ export const NotFoundContainer = styled.div`
min-height: 55vh;
`;
export const TimeContainer = styled.div`
export const TimeContainer = styled.div<Props>`
display: flex;
justify-content: flex-end;
${({ $panelType }): FlattenSimpleInterpolation =>
$panelType === PANEL_TYPES.TABLE
? css`
margin-bottom: 1rem;
`
: css``}
`;

View File

@ -2,8 +2,8 @@ import { Typography } from 'antd';
import { ChartData } from 'chart.js';
import { GraphOnClickHandler } from 'components/Graph';
import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent';
import { UpdateDashboard } from 'container/GridGraphLayout/utils';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useNotifications } from 'hooks/useNotifications';
@ -60,6 +60,7 @@ function GridCardGraph({
allowDelete,
allowClone,
allowEdit,
isQueryEnabled,
}: GridCardGraphProps): JSX.Element {
const { ref: graphRef, inView: isGraphVisible } = useInView({
threshold: 0,
@ -115,7 +116,7 @@ function GridCardGraph({
variables,
],
keepPreviousData: true,
enabled: isGraphVisible && !isEmptyWidget,
enabled: isGraphVisible && !isEmptyWidget && isQueryEnabled,
refetchOnMount: false,
onError: (error) => {
setErrorMessage(error.message);
@ -271,8 +272,8 @@ function GridCardGraph({
allowEdit={allowEdit}
/>
</div>
<GridGraphComponent
GRAPH_TYPES={widget?.panelTypes}
<GridPanelSwitch
panelType={widget?.panelTypes}
data={prevChartDataSetRef}
isStacked={widget?.isStacked}
opacity={widget?.opacity}
@ -280,6 +281,8 @@ function GridCardGraph({
name={name}
yAxisUnit={yAxisUnit}
onClickHandler={onClickHandler}
panelData={[]}
query={widget.query}
/>
</>
)}
@ -287,7 +290,7 @@ function GridCardGraph({
);
}
if (prevChartDataSetRef?.labels === undefined && queryResponse.isLoading) {
if (queryResponse.status === 'loading' || queryResponse.status === 'idle') {
return (
<span ref={graphRef}>
{!isEmpty(widget) && prevChartDataSetRef?.labels ? (
@ -307,8 +310,8 @@ function GridCardGraph({
allowEdit={allowEdit}
/>
</div>
<GridGraphComponent
GRAPH_TYPES={widget.panelTypes}
<GridPanelSwitch
panelType={widget.panelTypes}
data={prevChartDataSetRef}
isStacked={widget.isStacked}
opacity={widget.opacity}
@ -316,6 +319,8 @@ function GridCardGraph({
name={name}
yAxisUnit={yAxisUnit}
onClickHandler={onClickHandler}
panelData={[]}
query={widget.query}
/>
</>
) : (
@ -362,8 +367,8 @@ function GridCardGraph({
{!isEmptyLayout && getModals()}
{!isEmpty(widget) && !!queryResponse.data?.payload && (
<GridGraphComponent
GRAPH_TYPES={widget.panelTypes}
<GridPanelSwitch
panelType={widget.panelTypes}
data={chartData}
isStacked={widget.isStacked}
opacity={widget.opacity}
@ -372,6 +377,8 @@ function GridCardGraph({
yAxisUnit={yAxisUnit}
onDragSelect={onDragSelect}
onClickHandler={onClickHandler}
panelData={queryResponse.data?.payload.data.newResult.data.result || []}
query={widget.query}
/>
)}
@ -399,6 +406,7 @@ interface GridCardGraphProps extends DispatchProps {
allowDelete?: boolean;
allowClone?: boolean;
allowEdit?: boolean;
isQueryEnabled?: boolean;
}
GridCardGraph.defaultProps = {
@ -407,6 +415,7 @@ GridCardGraph.defaultProps = {
allowDelete: true,
allowClone: true,
allowEdit: true,
isQueryEnabled: true,
};
const mapDispatchToProps = (

View File

@ -1,4 +1,5 @@
import { PlusOutlined, SaveFilled } from '@ant-design/icons';
import { PANEL_TYPES } from 'constants/queryBuilder';
import useComponentPermission from 'hooks/useComponentPermission';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Dispatch, SetStateAction } from 'react';
@ -83,7 +84,7 @@ function GraphLayout({
key={currentWidget?.id || 'empty'} // don't change this key
data-grid={rest}
>
<Card>
<Card $panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}>
<Component setLayout={setLayout} />
</Card>
</CardContainer>

View File

@ -1,6 +1,7 @@
/* eslint-disable react/no-unstable-nested-components */
import updateDashboardApi from 'api/dashboard/update';
import { PANEL_TYPES } from 'constants/queryBuilder';
import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications';
import {
@ -260,7 +261,7 @@ function GridGraph(props: Props): JSX.Element {
{
data,
generateWidgetId: id,
graphType: 'EMPTY_WIDGET',
graphType: PANEL_TYPES.EMPTY_WIDGET,
selectedDashboard,
layout,
isRedirected: false,

View File

@ -1,11 +1,16 @@
import { Button as ButtonComponent, Card as CardComponent, Space } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { StyledCSS } from 'container/GantChart/Trace/styles';
import RGL, { WidthProvider } from 'react-grid-layout';
import styled, { css } from 'styled-components';
import styled, { css, FlattenSimpleInterpolation } from 'styled-components';
const ReactGridLayoutComponent = WidthProvider(RGL);
export const Card = styled(CardComponent)`
interface CardProps {
$panelType: PANEL_TYPES;
}
export const Card = styled(CardComponent)<CardProps>`
&&& {
height: 100%;
}
@ -13,6 +18,12 @@ export const Card = styled(CardComponent)`
.ant-card-body {
height: 95%;
padding: 0;
${({ $panelType }): FlattenSimpleInterpolation =>
$panelType === PANEL_TYPES.TABLE
? css`
padding-top: 1.8rem;
`
: css``}
}
`;
@ -21,6 +32,8 @@ interface Props {
}
export const CardContainer = styled.div<Props>`
overflow: auto;
:hover {
.react-resizable-handle {
position: absolute;
@ -44,6 +57,7 @@ export const CardContainer = styled.div<Props>`
background-image: ${(): string => `url("${uri}")`};
`;
}}
}
}
`;

View File

@ -1,7 +1,6 @@
import { NotificationInstance } from 'antd/es/notification/interface';
import updateDashboardApi from 'api/dashboard/update';
import { initialQueriesMap } from 'constants/queryBuilder';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { Layout } from 'react-grid-layout';
import store from 'store';
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
@ -69,7 +68,7 @@ export const UpdateDashboard = async (
interface UpdateDashboardProps {
data: Dashboard['data'];
graphType: GRAPH_TYPES;
graphType: PANEL_TYPES;
generateWidgetId: string;
layout: Layout[];
selectedDashboard: Dashboard;

View File

@ -0,0 +1,73 @@
import { PANEL_TYPES_COMPONENT_MAP } from 'constants/panelTypes';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
import { FC, memo, useMemo } from 'react';
import { GridPanelSwitchProps, PropsTypePropsMap } from './types';
function GridPanelSwitch({
panelType,
data,
title,
isStacked,
onClickHandler,
name,
yAxisUnit,
staticLine,
onDragSelect,
panelData,
query,
}: GridPanelSwitchProps): JSX.Element | null {
const currentProps: PropsTypePropsMap = useMemo(() => {
const result: PropsTypePropsMap = {
[PANEL_TYPES.TIME_SERIES]: {
type: 'line',
data,
title,
isStacked,
onClickHandler,
name,
yAxisUnit,
staticLine,
onDragSelect,
},
[PANEL_TYPES.VALUE]: {
title,
data,
yAxisUnit,
},
[PANEL_TYPES.TABLE]: { ...GRID_TABLE_CONFIG, data: panelData, query },
[PANEL_TYPES.LIST]: null,
[PANEL_TYPES.TRACE]: null,
[PANEL_TYPES.EMPTY_WIDGET]: null,
};
return result;
}, [
data,
isStacked,
name,
onClickHandler,
onDragSelect,
staticLine,
title,
yAxisUnit,
panelData,
query,
]);
const Component = PANEL_TYPES_COMPONENT_MAP[panelType] as FC<
PropsTypePropsMap[typeof panelType]
>;
const componentProps = useMemo(() => currentProps[panelType], [
panelType,
currentProps,
]);
if (!Component || !componentProps) return null;
// eslint-disable-next-line react/jsx-props-no-spreading
return <Component {...componentProps} />;
}
export default memo(GridPanelSwitch);

View File

@ -0,0 +1,36 @@
import { ChartData } from 'chart.js';
import {
GraphOnClickHandler,
GraphProps,
StaticLineProps,
} from 'components/Graph';
import { GridTableComponentProps } from 'container/GridTableComponent/types';
import { GridValueComponentProps } from 'container/GridValueComponent/types';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryDataV3 } from 'types/api/widgets/getQuery';
import { PANEL_TYPES } from '../../constants/queryBuilder';
export type GridPanelSwitchProps = {
panelType: PANEL_TYPES;
data: ChartData;
title?: string;
opacity?: string;
isStacked?: boolean;
onClickHandler?: GraphOnClickHandler;
name: string;
yAxisUnit?: string;
staticLine?: StaticLineProps;
onDragSelect?: (start: number, end: number) => void;
panelData: QueryDataV3[];
query: Query;
};
export type PropsTypePropsMap = {
[PANEL_TYPES.TIME_SERIES]: GraphProps;
[PANEL_TYPES.VALUE]: GridValueComponentProps;
[PANEL_TYPES.TABLE]: GridTableComponentProps;
[PANEL_TYPES.TRACE]: null;
[PANEL_TYPES.LIST]: null;
[PANEL_TYPES.EMPTY_WIDGET]: null;
};

View File

@ -0,0 +1,9 @@
import { TableProps } from 'antd';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
export const GRID_TABLE_CONFIG: Omit<
TableProps<RowData>,
'columns' | 'dataSource'
> = {
size: 'small',
};

View File

@ -0,0 +1,25 @@
import { QueryTable } from 'container/QueryTable';
import { memo } from 'react';
import { WrapperStyled } from './styles';
import { GridTableComponentProps } from './types';
function GridTableComponent({
data,
query,
...props
}: GridTableComponentProps): JSX.Element {
return (
<WrapperStyled>
<QueryTable
query={query}
queryTableData={data}
loading={false}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
</WrapperStyled>
);
}
export default memo(GridTableComponent);

View File

@ -0,0 +1,23 @@
import styled from 'styled-components';
export const WrapperStyled = styled.div`
height: 100%;
overflow: hidden;
& .ant-table-wrapper {
height: 100%;
}
& .ant-spin-nested-loading {
height: 100%;
}
& .ant-spin-container {
height: 100%;
display: flex;
flex-direction: column;
}
& .ant-table {
flex: 1;
overflow: auto;
}
`;

View File

@ -0,0 +1,10 @@
import { TableProps } from 'antd';
import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
export type GridTableComponentProps = { query: Query } & Pick<
LogsExplorerTableProps,
'data'
> &
Omit<TableProps<RowData>, 'columns' | 'dataSource'>;

View File

@ -0,0 +1,3 @@
import { GridValueComponentProps } from './types';
export const GridValueConfig: Pick<GridValueComponentProps, 'title'> = {};

View File

@ -0,0 +1,47 @@
import { Typography } from 'antd';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import ValueGraph from 'components/ValueGraph';
import { memo } from 'react';
import { useLocation } from 'react-router-dom';
import { TitleContainer, ValueContainer } from './styles';
import { GridValueComponentProps } from './types';
function GridValueComponent({
data,
title,
yAxisUnit,
}: GridValueComponentProps): JSX.Element {
const value = (((data.datasets[0] || []).data || [])[0] || 0) as number;
const location = useLocation();
const isDashboardPage = location.pathname.split('/').length === 3;
if (data.datasets.length === 0) {
return (
<ValueContainer isDashboardPage={isDashboardPage}>
<Typography>No Data</Typography>
</ValueContainer>
);
}
return (
<>
<TitleContainer isDashboardPage={isDashboardPage}>
<Typography>{title}</Typography>
</TitleContainer>
<ValueContainer isDashboardPage={isDashboardPage}>
<ValueGraph
value={
yAxisUnit
? getYAxisFormattedValue(String(value), yAxisUnit)
: value.toString()
}
/>
</ValueContainer>
</>
);
}
export default memo(GridValueComponent);

View File

@ -0,0 +1,7 @@
import { ChartData } from 'chart.js';
export type GridValueComponentProps = {
data: ChartData;
title?: string;
yAxisUnit?: string;
};

View File

@ -224,6 +224,7 @@ function ListOfAllDashboard(): JSX.Element {
key: t('import_grafana_json').toString(),
label: t('import_grafana_json'),
onClick: (): void => onModalHandler(true),
disabled: true,
});
return menuItems;

View File

@ -1,5 +1,9 @@
import { Button } from 'antd';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import {
initialQueriesMap,
OPERATORS,
PANEL_TYPES,
} from 'constants/queryBuilder';
import ExplorerOrderBy from 'container/ExplorerOrderBy';
import { QueryBuilder } from 'container/QueryBuilder';
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
@ -28,8 +32,14 @@ function LogExplorerQuerySection(): JSX.Element {
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const isTable = panelTypes === PANEL_TYPES.TABLE;
const isList = panelTypes === PANEL_TYPES.LIST;
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: isTable, isDisabled: true },
having: { isHidden: isList, isDisabled: true },
filters: {
customKey: 'body',
customOp: OPERATORS.CONTAINS,
},
};
return config;

View File

@ -17,7 +17,6 @@ import GoToTop from 'container/GoToTop';
import LogsExplorerChart from 'container/LogsExplorerChart';
import LogsExplorerList from 'container/LogsExplorerList';
import LogsExplorerTable from 'container/LogsExplorerTable';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
@ -179,7 +178,7 @@ function LogsExplorerViews(): JSX.Element {
}, []);
const getUpdateQuery = useCallback(
(newPanelType: GRAPH_TYPES): Query => {
(newPanelType: PANEL_TYPES): Query => {
let query = updateAllQueriesOperators(
currentQuery,
newPanelType,
@ -201,7 +200,7 @@ function LogsExplorerViews(): JSX.Element {
const handleChangeView = useCallback(
(type: string) => {
const newPanelType = type as GRAPH_TYPES;
const newPanelType = type as PANEL_TYPES;
if (newPanelType === panelType) return;

View File

@ -1,17 +1,19 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { Widgets } from 'types/api/dashboard/getAll';
import { v4 } from 'uuid';
export const getWidgetQueryBuilder = (
query: Widgets['query'],
import { GetWidgetQueryBuilderProps } from './types';
export const getWidgetQueryBuilder = ({
query,
title = '',
): Widgets => ({
panelTypes,
}: GetWidgetQueryBuilderProps): Widgets => ({
description: '',
id: v4(),
isStacked: false,
nullZeroValues: '',
opacity: '0',
panelTypes: PANEL_TYPES.TIME_SERIES,
panelTypes,
query,
timePreferance: 'GLOBAL_TIME',
title,

View File

@ -1,7 +1,11 @@
import { OPERATORS } from 'constants/queryBuilder';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, QueryBuilderData } from 'types/common/queryBuilder';
import {
DataSource,
MetricAggregateOperator,
QueryBuilderData,
} from 'types/common/queryBuilder';
import { DataType, FORMULA, MetricsType, WidgetKeys } from '../constant';
import { IServiceName } from '../Tabs/types';
@ -44,13 +48,14 @@ export const databaseCallsRPS = ({
];
const legends = [legend];
const dataSource = DataSource.METRICS;
return getQueryBuilderQueries({
autocompleteData,
groupBy,
legends,
filterItems,
dataSource: DataSource.METRICS,
dataSource,
});
};
@ -85,17 +90,36 @@ export const databaseCallsAvgDuration = ({
},
...tagFilterItems,
];
const additionalItemsB = additionalItemsA;
return getQueryBuilderQuerieswithFormula({
const autocompleteData: BaseAutocompleteData[] = [
autocompleteDataA,
autocompleteDataB,
];
const additionalItems: TagFilterItem[][] = [
additionalItemsA,
additionalItemsB,
legend: '',
disabled: true,
expression: FORMULA.DATABASE_CALLS_AVG_DURATION,
legendFormula: 'Average Duration',
additionalItemsA,
];
const legends = ['', ''];
const disabled = [true, true];
const legendFormula = 'Average Duration';
const expression = FORMULA.DATABASE_CALLS_AVG_DURATION;
const aggregateOperators = [
MetricAggregateOperator.SUM,
MetricAggregateOperator.SUM,
];
const dataSource = DataSource.METRICS;
return getQueryBuilderQuerieswithFormula({
autocompleteData,
additionalItems,
legends,
disabled,
expression,
legendFormula,
aggregateOperators,
dataSource,
});
};

View File

@ -1,10 +1,17 @@
import { OPERATORS } from 'constants/queryBuilder';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, QueryBuilderData } from 'types/common/queryBuilder';
import {
DataSource,
MetricAggregateOperator,
QueryBuilderData,
} from 'types/common/queryBuilder';
import { DataType, FORMULA, MetricsType, WidgetKeys } from '../constant';
import { IServiceName } from '../Tabs/types';
import {
ExternalCallDurationByAddressProps,
ExternalCallProps,
} from '../Tabs/types';
import {
getQueryBuilderQueries,
getQueryBuilderQuerieswithFormula,
@ -36,6 +43,7 @@ export const externalCallErrorPercent = ({
isColumn: true,
type: null,
};
const additionalItemsA: TagFilterItem[] = [
{
id: '',
@ -71,23 +79,38 @@ export const externalCallErrorPercent = ({
type: MetricsType.Resource,
},
op: OPERATORS.IN,
value: [`${servicename}`],
value: [servicename],
},
...tagFilterItems,
];
const legendFormula = legend;
const expression = FORMULA.ERROR_PERCENTAGE;
const disabled = true;
return getQueryBuilderQuerieswithFormula({
const autocompleteData: BaseAutocompleteData[] = [
autocompleteDataA,
autocompleteDataB,
];
const additionalItems: TagFilterItem[][] = [
additionalItemsA,
additionalItemsB,
legend,
];
const legends = Array(2).fill(legend);
const aggregateOperators = Array(2).fill(MetricAggregateOperator.SUM);
const disabled = Array(2).fill(true);
const dataSource = DataSource.METRICS;
return getQueryBuilderQuerieswithFormula({
autocompleteData,
additionalItems,
legends,
groupBy,
disabled,
expression,
legendFormula,
aggregateOperators,
dataSource,
});
};
@ -107,10 +130,11 @@ export const externalCallDuration = ({
key: WidgetKeys.SignozExternalCallLatencyCount,
type: null,
};
const expression = FORMULA.DATABASE_CALLS_AVG_DURATION;
const legendFormula = 'Average Duration';
const legend = '';
const disabled = true;
const disabled = Array(2).fill(true);
const additionalItemsA: TagFilterItem[] = [
{
id: '',
@ -125,17 +149,29 @@ export const externalCallDuration = ({
},
...tagFilterItems,
];
const additionalItemsB = additionalItemsA;
return getQueryBuilderQuerieswithFormula({
const autocompleteData: BaseAutocompleteData[] = [
autocompleteDataA,
autocompleteDataB,
];
const additionalItems: TagFilterItem[][] = [
additionalItemsA,
additionalItemsB,
legend,
additionalItemsA,
];
const legends = Array(2).fill(legend);
const aggregateOperators = Array(2).fill(MetricAggregateOperator.SUM);
return getQueryBuilderQuerieswithFormula({
autocompleteData,
additionalItems,
legends,
disabled,
expression,
legendFormula,
aggregateOperators,
dataSource: DataSource.METRICS,
});
};
@ -169,13 +205,15 @@ export const externalCallRpsByAddress = ({
],
];
const legends: string[] = [legend];
const legends = [legend];
const dataSource = DataSource.METRICS;
return getQueryBuilderQueries({
autocompleteData,
groupBy,
legends,
filterItems,
dataSource: DataSource.METRICS,
dataSource,
});
};
@ -198,7 +236,7 @@ export const externalCallDurationByAddress = ({
};
const expression = FORMULA.DATABASE_CALLS_AVG_DURATION;
const legendFormula = legend;
const disabled = true;
const disabled = [true, true];
const additionalItemsA: TagFilterItem[] = [
{
id: '',
@ -213,26 +251,30 @@ export const externalCallDurationByAddress = ({
},
...tagFilterItems,
];
const additionalItemsB = additionalItemsA;
return getQueryBuilderQuerieswithFormula({
const autocompleteData: BaseAutocompleteData[] = [
autocompleteDataA,
autocompleteDataB,
];
const additionalItems: TagFilterItem[][] = [
additionalItemsA,
additionalItemsB,
legend,
additionalItemsA,
];
const legends = Array(2).fill(legend);
const aggregateOperators = Array(2).fill(MetricAggregateOperator.SUM_RATE);
const dataSource = DataSource.METRICS;
return getQueryBuilderQuerieswithFormula({
autocompleteData,
additionalItems,
legends,
groupBy,
disabled,
expression,
legendFormula,
aggregateOperators,
dataSource,
});
};
interface ExternalCallDurationByAddressProps extends ExternalCallProps {
legend: string;
}
export interface ExternalCallProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
}

View File

@ -1,20 +1,21 @@
import {
alphabet,
initialFormulaBuilderFormValues,
initialQueryBuilderFormValuesMap,
} from 'constants/queryBuilder';
import getStep from 'lib/getStep';
import store from 'store';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
DataSource,
MetricAggregateOperator,
QueryBuilderData,
} from 'types/common/queryBuilder';
import {
BuilderQueriesProps,
BuilderQuerieswithFormulaProps,
} from '../Tabs/types';
export const getQueryBuilderQueries = ({
autocompleteData,
groupBy = [],
@ -61,15 +62,15 @@ export const getQueryBuilderQueries = ({
});
export const getQueryBuilderQuerieswithFormula = ({
autocompleteDataA,
autocompleteDataB,
additionalItemsA,
additionalItemsB,
legend,
autocompleteData,
additionalItems,
legends,
groupBy = [],
disabled,
expression,
legendFormula,
aggregateOperators,
dataSource,
}: BuilderQuerieswithFormulaProps): QueryBuilderData => ({
queryFormulas: [
{
@ -78,66 +79,25 @@ export const getQueryBuilderQuerieswithFormula = ({
legend: legendFormula,
},
],
queryData: [
{
...initialQueryBuilderFormValuesMap.metrics,
aggregateOperator: MetricAggregateOperator.SUM_RATE,
disabled,
groupBy,
legend,
aggregateAttribute: autocompleteDataA,
reduceTo: 'sum',
filters: {
items: additionalItemsA,
op: 'AND',
},
stepInterval: getStep({
end: store.getState().globalTime.maxTime,
inputFormat: 'ns',
start: store.getState().globalTime.minTime,
}),
queryData: autocompleteData.map((_, index) => ({
...initialQueryBuilderFormValuesMap.metrics,
aggregateOperator: aggregateOperators[index],
disabled: disabled[index],
groupBy,
legend: legends[index],
aggregateAttribute: autocompleteData[index],
queryName: alphabet[index],
expression: alphabet[index],
reduceTo: 'sum',
filters: {
items: additionalItems[index],
op: 'AND',
},
{
...initialQueryBuilderFormValuesMap.metrics,
aggregateOperator: MetricAggregateOperator.SUM_RATE,
disabled,
groupBy,
legend,
aggregateAttribute: autocompleteDataB,
queryName: 'B',
expression: 'B',
reduceTo: 'sum',
filters: {
items: additionalItemsB,
op: 'AND',
},
stepInterval: getStep({
end: store.getState().globalTime.maxTime,
inputFormat: 'ns',
start: store.getState().globalTime.minTime,
}),
},
],
stepInterval: getStep({
end: store.getState().globalTime.maxTime,
inputFormat: 'ns',
start: store.getState().globalTime.minTime,
}),
dataSource,
})),
});
interface BuilderQueriesProps {
autocompleteData: BaseAutocompleteData[];
groupBy?: BaseAutocompleteData[];
legends: string[];
filterItems: TagFilterItem[][];
aggregateOperator?: string[];
dataSource: DataSource;
queryNameAndExpression?: string[];
}
interface BuilderQuerieswithFormulaProps {
autocompleteDataA: BaseAutocompleteData;
autocompleteDataB: BaseAutocompleteData;
legend: string;
disabled: boolean;
groupBy?: BaseAutocompleteData[];
expression: string;
legendFormula: string;
additionalItemsA: TagFilterItem[];
additionalItemsB: TagFilterItem[];
}

View File

@ -1,7 +1,11 @@
import { OPERATORS } from 'constants/queryBuilder';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, QueryBuilderData } from 'types/common/queryBuilder';
import {
DataSource,
MetricAggregateOperator,
QueryBuilderData,
} from 'types/common/queryBuilder';
import {
DataType,
@ -14,7 +18,7 @@ import {
QUERYNAME_AND_EXPRESSION,
WidgetKeys,
} from '../constant';
import { IServiceName } from '../Tabs/types';
import { LatencyProps, OperationPerSecProps } from '../Tabs/types';
import {
getQueryBuilderQueries,
getQueryBuilderQuerieswithFormula,
@ -35,9 +39,7 @@ export const latency = ({
type: isSpanMetricEnable ? null : MetricsType.Tag,
};
const autocompleteData: BaseAutocompleteData[] = Array(3).fill(
newAutoCompleteData,
);
const autocompleteData = Array(3).fill(newAutoCompleteData);
const filterItem: TagFilterItem[] = [
{
@ -65,17 +67,21 @@ export const latency = ({
...tagFilterItems,
];
const filterItems: TagFilterItem[][] = Array(3).fill([...filterItem]);
const filterItems = Array(3).fill([...filterItem]);
const legends = LATENCY_AGGREGATEOPERATOR;
const aggregateOperator = isSpanMetricEnable
? LATENCY_AGGREGATEOPERATOR_SPAN_METRICS
: LATENCY_AGGREGATEOPERATOR;
const dataSource = isSpanMetricEnable ? DataSource.METRICS : DataSource.TRACES;
const queryNameAndExpression = QUERYNAME_AND_EXPRESSION;
return getQueryBuilderQueries({
autocompleteData,
legends: LATENCY_AGGREGATEOPERATOR,
legends,
filterItems,
aggregateOperator: isSpanMetricEnable
? LATENCY_AGGREGATEOPERATOR_SPAN_METRICS
: LATENCY_AGGREGATEOPERATOR,
dataSource: isSpanMetricEnable ? DataSource.METRICS : DataSource.TRACES,
queryNameAndExpression: QUERYNAME_AND_EXPRESSION,
aggregateOperator,
dataSource,
queryNameAndExpression,
});
};
@ -121,11 +127,14 @@ export const operationPerSec = ({
],
];
const legends = OPERATION_LEGENDS;
const dataSource = DataSource.METRICS;
return getQueryBuilderQueries({
autocompleteData,
legends: OPERATION_LEGENDS,
legends,
filterItems,
dataSource: DataSource.METRICS,
dataSource,
});
};
@ -146,6 +155,9 @@ export const errorPercentage = ({
isColumn: true,
type: null,
};
const autocompleteData = [autocompleteDataA, autocompleteDataB];
const additionalItemsA: TagFilterItem[] = [
{
id: '',
@ -209,27 +221,25 @@ export const errorPercentage = ({
...tagFilterItems,
];
const additionalItems = [additionalItemsA, additionalItemsB];
const legends = [GraphTitle.ERROR_PERCENTAGE];
const disabled = [true, true];
const expression = FORMULA.ERROR_PERCENTAGE;
const legendFormula = GraphTitle.ERROR_PERCENTAGE;
const aggregateOperators = [
MetricAggregateOperator.SUM_RATE,
MetricAggregateOperator.SUM_RATE,
];
const dataSource = DataSource.METRICS;
return getQueryBuilderQuerieswithFormula({
autocompleteDataA,
autocompleteDataB,
additionalItemsA,
additionalItemsB,
legend: GraphTitle.ERROR_PERCENTAGE,
disabled: true,
expression: FORMULA.ERROR_PERCENTAGE,
legendFormula: GraphTitle.ERROR_PERCENTAGE,
autocompleteData,
additionalItems,
legends,
disabled,
expression,
legendFormula,
aggregateOperators,
dataSource,
});
};
export interface OperationPerSecProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
topLevelOperations: string[];
}
export interface LatencyProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
isSpanMetricEnable?: boolean;
topLevelOperationsRoute: string[];
}

View File

@ -0,0 +1,142 @@
import { OPERATORS } from 'constants/queryBuilder';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import {
DataSource,
MetricAggregateOperator,
QueryBuilderData,
} from 'types/common/queryBuilder';
import {
DataType,
GraphTitle,
KeyOperationTableHeader,
MetricsType,
WidgetKeys,
} from '../constant';
import { TopOperationQueryFactoryProps } from '../Tabs/types';
import { getQueryBuilderQuerieswithFormula } from './MetricsPageQueriesFactory';
export const topOperationQueries = ({
servicename,
}: TopOperationQueryFactoryProps): QueryBuilderData => {
const latencyAutoCompleteData: BaseAutocompleteData = {
key: WidgetKeys.Signoz_latency_bucket,
dataType: DataType.FLOAT64,
isColumn: true,
type: null,
};
const errorRateAutoCompleteData: BaseAutocompleteData = {
key: WidgetKeys.SignozCallsTotal,
dataType: DataType.FLOAT64,
isColumn: true,
type: null,
};
const numOfCallAutoCompleteData: BaseAutocompleteData = {
key: WidgetKeys.SignozLatencyCount,
dataType: DataType.FLOAT64,
isColumn: true,
type: null,
};
const latencyAndNumberOfCallAdditionalItems: TagFilterItem[] = [
{
id: '',
key: {
key: WidgetKeys.Service_name,
dataType: DataType.STRING,
isColumn: false,
type: MetricsType.Resource,
},
value: [servicename],
op: OPERATORS.IN,
},
];
const errorRateAdditionalItemsA: TagFilterItem[] = [
{
id: '',
key: {
dataType: DataType.STRING,
isColumn: false,
key: WidgetKeys.Service_name,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
value: [servicename],
},
{
id: '',
key: {
dataType: DataType.INT64,
isColumn: false,
key: WidgetKeys.StatusCode,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
value: ['STATUS_CODE_ERROR'],
},
];
const errorRateAdditionalItemsB = latencyAndNumberOfCallAdditionalItems;
const groupBy: BaseAutocompleteData[] = [
{
dataType: DataType.STRING,
isColumn: false,
key: WidgetKeys.Operation,
type: MetricsType.Tag,
},
];
const autocompleteData = [
latencyAutoCompleteData,
latencyAutoCompleteData,
latencyAutoCompleteData,
errorRateAutoCompleteData,
errorRateAutoCompleteData,
numOfCallAutoCompleteData,
];
const additionalItems = [
latencyAndNumberOfCallAdditionalItems,
latencyAndNumberOfCallAdditionalItems,
latencyAndNumberOfCallAdditionalItems,
errorRateAdditionalItemsA,
errorRateAdditionalItemsB,
latencyAndNumberOfCallAdditionalItems,
];
const disabled = [false, false, false, true, true, false];
const legends = [
KeyOperationTableHeader.P50,
KeyOperationTableHeader.P90,
KeyOperationTableHeader.P99,
KeyOperationTableHeader.ERROR_RATE,
KeyOperationTableHeader.ERROR_RATE,
KeyOperationTableHeader.NUM_OF_CALLS,
];
const aggregateOperators = [
MetricAggregateOperator.HIST_QUANTILE_50,
MetricAggregateOperator.HIST_QUANTILE_90,
MetricAggregateOperator.HIST_QUANTILE_99,
MetricAggregateOperator.SUM_RATE,
MetricAggregateOperator.SUM_RATE,
MetricAggregateOperator.SUM_RATE,
];
const expression = 'D*100/E';
const legendFormula = GraphTitle.ERROR_PERCENTAGE;
const dataSource = DataSource.METRICS;
return getQueryBuilderQuerieswithFormula({
autocompleteData,
additionalItems,
disabled,
legends,
aggregateOperators,
expression,
legendFormula,
dataSource,
groupBy,
});
};

View File

@ -1,4 +1,5 @@
import { Col } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridGraphLayout/Graph/';
import {
databaseCallsAvgDuration,
@ -50,8 +51,8 @@ function DBCall(): JSX.Element {
const databaseCallsRPSWidget = useMemo(
() =>
getWidgetQueryBuilder(
{
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: databaseCallsRPS({
@ -62,14 +63,15 @@ function DBCall(): JSX.Element {
clickhouse_sql: [],
id: uuid(),
},
GraphTitle.DATABASE_CALLS_RPS,
),
title: GraphTitle.DATABASE_CALLS_RPS,
panelTypes: PANEL_TYPES.TIME_SERIES,
}),
[servicename, tagFilterItems],
);
const databaseCallsAverageDurationWidget = useMemo(
() =>
getWidgetQueryBuilder(
{
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: databaseCallsAvgDuration({
@ -79,8 +81,9 @@ function DBCall(): JSX.Element {
clickhouse_sql: [],
id: uuid(),
},
GraphTitle.DATABASE_CALLS_AVG_DURATION,
),
title: GraphTitle.DATABASE_CALLS_AVG_DURATION,
panelTypes: PANEL_TYPES.TIME_SERIES,
}),
[servicename, tagFilterItems],
);

View File

@ -1,4 +1,5 @@
import { Col } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridGraphLayout/Graph/';
import {
externalCallDuration,
@ -41,8 +42,8 @@ function External(): JSX.Element {
const externalCallErrorWidget = useMemo(
() =>
getWidgetQueryBuilder(
{
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: externalCallErrorPercent({
@ -53,8 +54,9 @@ function External(): JSX.Element {
clickhouse_sql: [],
id: uuid(),
},
GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
),
title: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
}),
[servicename, tagFilterItems],
);
@ -65,8 +67,8 @@ function External(): JSX.Element {
const externalCallDurationWidget = useMemo(
() =>
getWidgetQueryBuilder(
{
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: externalCallDuration({
@ -76,15 +78,16 @@ function External(): JSX.Element {
clickhouse_sql: [],
id: uuid(),
},
GraphTitle.EXTERNAL_CALL_DURATION,
),
title: GraphTitle.EXTERNAL_CALL_DURATION,
panelTypes: PANEL_TYPES.TIME_SERIES,
}),
[servicename, tagFilterItems],
);
const externalCallRPSWidget = useMemo(
() =>
getWidgetQueryBuilder(
{
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: externalCallRpsByAddress({
@ -95,15 +98,16 @@ function External(): JSX.Element {
clickhouse_sql: [],
id: uuid(),
},
GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
),
title: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
panelTypes: PANEL_TYPES.TIME_SERIES,
}),
[servicename, tagFilterItems],
);
const externalCallDurationAddressWidget = useMemo(
() =>
getWidgetQueryBuilder(
{
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: externalCallDurationByAddress({
@ -114,8 +118,9 @@ function External(): JSX.Element {
clickhouse_sql: [],
id: uuid(),
},
GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
),
title: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
panelTypes: PANEL_TYPES.TIME_SERIES,
}),
[servicename, tagFilterItems],
);

View File

@ -2,10 +2,13 @@ import getTopLevelOperations, {
ServiceDataProps,
} from 'api/metrics/getTopLevelOperations';
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { routeConfig } from 'container/SideNav/config';
import { getQueryString } from 'container/SideNav/helper';
import useFeatureFlag from 'hooks/useFeatureFlag';
import useResourceAttribute from 'hooks/useResourceAttribute';
import {
convertRawQueriesToTraceSelectedTags,
@ -29,10 +32,11 @@ import {
errorPercentage,
operationPerSec,
} from '../MetricsPageQueries/OverviewQueries';
import { Col, Row } from '../styles';
import { Card, Col, Row } from '../styles';
import ServiceOverview from './Overview/ServiceOverview';
import TopLevelOperation from './Overview/TopLevelOperations';
import TopOperation from './Overview/TopOperation';
import TopOperationMetrics from './Overview/TopOperationMetrics';
import { Button } from './styles';
import { IServiceName } from './types';
import {
@ -53,6 +57,8 @@ function Application(): JSX.Element {
() => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
[queries],
);
const isSpanMetricEnabled = useFeatureFlag(FeatureKeys.USE_SPAN_METRICS)
?.active;
const handleSetTimeStamp = useCallback((selectTime: number) => {
setSelectedTimeStamp(selectTime);
@ -104,8 +110,8 @@ function Application(): JSX.Element {
const operationPerSecWidget = useMemo(
() =>
getWidgetQueryBuilder(
{
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: operationPerSec({
@ -116,15 +122,16 @@ function Application(): JSX.Element {
clickhouse_sql: [],
id: uuid(),
},
GraphTitle.RATE_PER_OPS,
),
title: GraphTitle.RATE_PER_OPS,
panelTypes: PANEL_TYPES.TIME_SERIES,
}),
[servicename, tagFilterItems, topLevelOperationsRoute],
);
const errorPercentageWidget = useMemo(
() =>
getWidgetQueryBuilder(
{
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: errorPercentage({
@ -135,8 +142,9 @@ function Application(): JSX.Element {
clickhouse_sql: [],
id: uuid(),
},
GraphTitle.ERROR_PERCENTAGE,
),
title: GraphTitle.ERROR_PERCENTAGE,
panelTypes: PANEL_TYPES.TIME_SERIES,
}),
[servicename, tagFilterItems, topLevelOperationsRoute],
);
@ -239,7 +247,9 @@ function Application(): JSX.Element {
</Col>
<Col span={12}>
<TopOperation />
<Card>
{isSpanMetricEnabled ? <TopOperationMetrics /> : <TopOperation />}
</Card>
</Col>
</Row>
</>

View File

@ -1,4 +1,5 @@
import { FeatureKeys } from 'constants/features';
import { PANEL_TYPES } from 'constants/queryBuilder';
import Graph from 'container/GridGraphLayout/Graph/';
import { GraphTitle } from 'container/MetricsApplication/constant';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
@ -31,8 +32,8 @@ function ServiceOverview({
const latencyWidget = useMemo(
() =>
getWidgetQueryBuilder(
{
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: latency({
@ -44,11 +45,14 @@ function ServiceOverview({
clickhouse_sql: [],
id: uuid(),
},
GraphTitle.LATENCY,
),
title: GraphTitle.LATENCY,
panelTypes: PANEL_TYPES.TIME_SERIES,
}),
[servicename, tagFilterItems, isSpanMetricEnable, topLevelOperationsRoute],
);
const isQueryEnabled = topLevelOperationsRoute.length > 0;
return (
<>
<Button
@ -74,6 +78,7 @@ function ServiceOverview({
allowClone={false}
allowDelete={false}
allowEdit={false}
isQueryEnabled={isQueryEnabled}
/>
</GraphContainer>
</Card>

View File

@ -0,0 +1,39 @@
import { Tooltip, Typography } from 'antd';
import { navigateToTrace } from 'container/MetricsApplication/utils';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
function ColumnWithLink({
servicename,
minTime,
maxTime,
selectedTraceTags,
record,
}: LinkColumnProps): JSX.Element {
const text = record.toString();
const handleOnClick = (operation: string) => (): void => {
navigateToTrace({
servicename,
operation,
minTime,
maxTime,
selectedTraceTags,
});
};
return (
<Tooltip placement="topLeft" title={text}>
<Typography.Link onClick={handleOnClick(text)}>{text}</Typography.Link>
</Tooltip>
);
}
interface LinkColumnProps {
servicename: string;
minTime: number;
maxTime: number;
selectedTraceTags: string;
record: RowData;
}
export default ColumnWithLink;

View File

@ -0,0 +1,11 @@
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { ReactNode } from 'react';
import { TableRendererProps } from '../../types';
export const getTableColumnRenderer = ({
columnName,
renderFunction,
}: TableRendererProps): Record<string, (record: RowData) => ReactNode> => ({
[columnName]: renderFunction,
});

View File

@ -1,6 +1,5 @@
import getTopOperations from 'api/metrics/getTopOperations';
import Spinner from 'components/Spinner';
import { Card } from 'container/MetricsApplication/styles';
import TopOperationsTable from 'container/MetricsApplication/TopOperationsTable';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
@ -35,11 +34,13 @@ function TopOperation(): JSX.Element {
}),
});
const topOperationData = data || [];
return (
<Card>
<>
{isLoading && <Spinner size="large" tip="Loading..." height="40vh" />}
{!isLoading && <TopOperationsTable data={data || []} />}
</Card>
{!isLoading && <TopOperationsTable data={topOperationData} />}
</>
);
}

View File

@ -0,0 +1,122 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { topOperationQueries } from 'container/MetricsApplication/MetricsPageQueries/TopOperationQueries';
import { QueryTable } from 'container/QueryTable';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { isEmpty } from 'lodash-es';
import { ReactNode, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import { IServiceName } from '../types';
import { title } from './config';
import ColumnWithLink from './TableRenderer/ColumnWithLink';
import { getTableColumnRenderer } from './TableRenderer/TableColumnRenderer';
function TopOperationMetrics(): JSX.Element {
const { servicename } = useParams<IServiceName>();
const [errorMessage, setErrorMessage] = useState<string | undefined>('');
const { minTime, maxTime, selectedTime: globalSelectedInterval } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const { queries } = useResourceAttribute();
const selectedTraceTags = JSON.stringify(
convertRawQueriesToTraceSelectedTags(queries) || [],
);
const keyOperationWidget = useMemo(
() =>
getWidgetQueryBuilder({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: topOperationQueries({
servicename,
}),
clickhouse_sql: [],
id: uuid(),
},
panelTypes: PANEL_TYPES.TABLE,
}),
[servicename],
);
const updatedQuery = useStepInterval(keyOperationWidget.query);
const isEmptyWidget = useMemo(
() => keyOperationWidget.id === 'empty' || isEmpty(keyOperationWidget),
[keyOperationWidget],
);
const { data, isLoading } = useGetQueryRange(
{
selectedTime: keyOperationWidget?.timePreferance,
graphType: keyOperationWidget?.panelTypes,
query: updatedQuery,
globalSelectedInterval,
variables: getDashboardVariables(),
},
{
queryKey: [
`GetMetricsQueryRange-${keyOperationWidget?.timePreferance}-${globalSelectedInterval}-${keyOperationWidget?.id}`,
keyOperationWidget,
maxTime,
minTime,
globalSelectedInterval,
],
keepPreviousData: true,
enabled: !isEmptyWidget,
refetchOnMount: false,
onError: (error) => {
setErrorMessage(error.message);
},
},
);
const queryTableData = data?.payload.data.newResult.data.result || [];
const renderColumnCell = useMemo(
() =>
getTableColumnRenderer({
columnName: 'operation',
renderFunction: (record: RowData): ReactNode => (
<ColumnWithLink
servicename={servicename}
minTime={minTime}
maxTime={maxTime}
selectedTraceTags={selectedTraceTags}
record={record}
/>
),
}),
[servicename, minTime, maxTime, selectedTraceTags],
);
if (errorMessage) {
return <div>{errorMessage}</div>;
}
return (
<QueryTable
title={title}
query={updatedQuery}
queryTableData={queryTableData}
loading={isLoading}
renderColumnCell={renderColumnCell}
/>
);
}
export default TopOperationMetrics;

View File

@ -0,0 +1 @@
export const title = (): string => 'Key Operations';

View File

@ -1,3 +1,62 @@
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { ReactNode } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
export interface IServiceName {
servicename: string;
}
export interface TopOperationQueryFactoryProps {
servicename: IServiceName['servicename'];
}
export interface ExternalCallDurationByAddressProps extends ExternalCallProps {
legend: string;
}
export interface ExternalCallProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
}
export interface BuilderQueriesProps {
autocompleteData: BaseAutocompleteData[];
groupBy?: BaseAutocompleteData[];
legends: string[];
filterItems: TagFilterItem[][];
aggregateOperator?: string[];
dataSource: DataSource;
queryNameAndExpression?: string[];
}
export interface BuilderQuerieswithFormulaProps {
autocompleteData: BaseAutocompleteData[];
legends: string[];
disabled: boolean[];
groupBy?: BaseAutocompleteData[];
expression: string;
legendFormula: string;
additionalItems: TagFilterItem[][];
aggregateOperators: MetricAggregateOperator[];
dataSource: DataSource;
}
export interface OperationPerSecProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
topLevelOperations: string[];
}
export interface LatencyProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
isSpanMetricEnable?: boolean;
topLevelOperationsRoute: string[];
}
export interface TableRendererProps {
columnName: string;
renderFunction: (record: RowData) => ReactNode;
}

View File

@ -1,17 +1,14 @@
import { Tooltip, Typography } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import { ResizeTable } from 'components/ResizeTable';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import history from 'lib/history';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getErrorRate } from './utils';
import { getErrorRate, navigateToTrace } from './utils';
function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
@ -28,16 +25,15 @@ function TopOperationsTable(props: TopOperationsTableProps): JSX.Element {
const params = useParams<{ servicename: string }>();
const handleOnClick = (operation: string): void => {
const urlParams = new URLSearchParams();
const { servicename } = params;
urlParams.set(QueryParams.startTime, (minTime / 1000000).toString());
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
history.push(
`${
ROUTES.TRACE
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"operation":["${operation}"]}&filterToFetchData=["duration","status","serviceName","operation"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false,"operation":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&spanAggregateCurrentPage=1`,
);
navigateToTrace({
servicename,
operation,
minTime,
maxTime,
selectedTraceTags,
});
};
const columns: ColumnsType<TopOperationList> = [

View File

@ -28,6 +28,14 @@ export enum GraphTitle {
EXTERNAL_CALL_DURATION_BY_ADDRESS = 'External Call duration(by Address)',
}
export enum KeyOperationTableHeader {
P50 = 'P50',
P90 = 'P90',
P99 = 'P99',
NUM_OF_CALLS = 'Number of Calls',
ERROR_RATE = 'Error Rate',
}
export enum DataType {
STRING = 'string',
FLOAT64 = 'float64',

View File

@ -0,0 +1,15 @@
import { Widgets } from 'types/api/dashboard/getAll';
export interface GetWidgetQueryBuilderProps {
query: Widgets['query'];
title?: string;
panelTypes: Widgets['panelTypes'];
}
export interface NavigateToTraceProps {
servicename: string;
operation: string;
minTime: number;
maxTime: number;
selectedTraceTags: string;
}

View File

@ -1,4 +1,26 @@
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { TopOperationList } from './TopOperationsTable';
import { NavigateToTraceProps } from './types';
export const getErrorRate = (list: TopOperationList): number =>
(list.errorCount / list.numCalls) * 100;
export const navigateToTrace = ({
servicename,
operation,
minTime,
maxTime,
selectedTraceTags,
}: NavigateToTraceProps): void => {
const urlParams = new URLSearchParams();
urlParams.set(QueryParams.startTime, (minTime / 1000000).toString());
urlParams.set(QueryParams.endTime, (maxTime / 1000000).toString());
history.push(
`${
ROUTES.TRACE
}?${urlParams.toString()}&selected={"serviceName":["${servicename}"],"operation":["${operation}"]}&filterToFetchData=["duration","status","serviceName","operation"]&spanAggregateCurrentPage=1&selectedTags=${selectedTraceTags}&&isFilterExclude={"serviceName":false,"operation":false}&userSelectedFilter={"status":["error","ok"],"serviceName":["${servicename}"],"operation":["${operation}"]}&spanAggregateCurrentPage=1`,
);
};

View File

@ -1,70 +0,0 @@
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
import { ReactElement } from 'react';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import {
combineReducers,
legacy_createStore as createStore,
Store,
} from 'redux';
import { InitialValue } from '../../store/reducers/metric';
import Metrics from './index';
const rootReducer = combineReducers({
metrics: (state = InitialValue) => state,
});
const mockStore = createStore(rootReducer);
const renderWithReduxAndRouter = (mockStore: Store) => (
component: ReactElement,
): RenderResult =>
render(
<BrowserRouter>
<Provider store={mockStore}>{component}</Provider>
</BrowserRouter>,
);
describe('Metrics Component', () => {
it('renders without errors', async () => {
renderWithReduxAndRouter(mockStore)(<Metrics />);
await waitFor(() => {
expect(screen.getByText(/application/i)).toBeInTheDocument();
expect(screen.getByText(/p99 latency \(in ms\)/i)).toBeInTheDocument();
expect(screen.getByText(/error rate \(% of total\)/i)).toBeInTheDocument();
expect(screen.getByText(/operations per second/i)).toBeInTheDocument();
});
});
it('renders loading when required conditions are met', async () => {
const customStore = createStore(rootReducer, {
metrics: {
services: [],
loading: true,
error: false,
},
});
const { container } = renderWithReduxAndRouter(customStore)(<Metrics />);
const spinner = container.querySelector('.ant-spin-nested-loading');
expect(spinner).toBeInTheDocument();
});
it('renders no data when required conditions are met', async () => {
const customStore = createStore(rootReducer, {
metrics: {
services: [],
loading: false,
error: false,
},
});
renderWithReduxAndRouter(customStore)(<Metrics />);
expect(screen.getByText('No data')).toBeInTheDocument();
});
});

View File

@ -1,169 +0,0 @@
import { blue } from '@ant-design/colors';
import { SearchOutlined } from '@ant-design/icons';
import { Button, Card, Input, Space } from 'antd';
import type { ColumnsType, ColumnType } from 'antd/es/table';
import type {
FilterConfirmProps,
FilterDropdownProps,
} from 'antd/es/table/interface';
import localStorageGet from 'api/browser/localstorage/get';
import localStorageSet from 'api/browser/localstorage/set';
import { ResizeTable } from 'components/ResizeTable';
import { SKIP_ONBOARDING } from 'constants/onboarding';
import ROUTES from 'constants/routes';
import { routeConfig } from 'container/SideNav/config';
import { getQueryString } from 'container/SideNav/helper';
import { useCallback, useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { Link, useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { ServicesList } from 'types/api/metrics/getService';
import MetricReducer from 'types/reducer/metrics';
import SkipBoardModal from './SkipOnBoardModal';
import { Container, Name } from './styles';
function Metrics(): JSX.Element {
const { search } = useLocation();
const [skipOnboarding, setSkipOnboarding] = useState(
localStorageGet(SKIP_ONBOARDING) === 'true',
);
const { services, loading, error } = useSelector<AppState, MetricReducer>(
(state) => state.metrics,
);
const onContinueClick = (): void => {
localStorageSet(SKIP_ONBOARDING, 'true');
setSkipOnboarding(true);
};
const handleSearch = (confirm: (param?: FilterConfirmProps) => void): void => {
confirm();
};
const FilterIcon: ColumnType<DataProps>['filterIcon'] = useCallback(
(filtered: boolean) => (
<SearchOutlined
style={{
color: filtered ? blue[6] : undefined,
}}
/>
),
[],
);
const filterDropdown = useCallback(
({ setSelectedKeys, selectedKeys, confirm }: FilterDropdownProps) => (
<Card size="small">
<Space align="start" direction="vertical">
<Input
placeholder="Search by service"
value={selectedKeys[0]}
onChange={(e): void =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
allowClear
onPressEnter={(): void => handleSearch(confirm)}
/>
<Button
type="primary"
onClick={(): void => handleSearch(confirm)}
icon={<SearchOutlined />}
size="small"
>
Search
</Button>
</Space>
</Card>
),
[],
);
type DataIndex = keyof ServicesList;
const getColumnSearchProps = useCallback(
(dataIndex: DataIndex): ColumnType<DataProps> => ({
filterDropdown,
filterIcon: FilterIcon,
onFilter: (value: string | number | boolean, record: DataProps): boolean =>
record[dataIndex]
.toString()
.toLowerCase()
.includes(value.toString().toLowerCase()),
render: (metrics: string): JSX.Element => {
const urlParams = new URLSearchParams(search);
const avialableParams = routeConfig[ROUTES.SERVICE_METRICS];
const queryString = getQueryString(avialableParams, urlParams);
return (
<Link to={`${ROUTES.APPLICATION}/${metrics}?${queryString.join('')}`}>
<Name>{metrics}</Name>
</Link>
);
},
}),
[filterDropdown, FilterIcon, search],
);
const columns: ColumnsType<DataProps> = useMemo(
() => [
{
title: 'Application',
dataIndex: 'serviceName',
width: 200,
key: 'serviceName',
...getColumnSearchProps('serviceName'),
},
{
title: 'P99 latency (in ms)',
dataIndex: 'p99',
key: 'p99',
width: 150,
defaultSortOrder: 'descend',
sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99,
render: (value: number): string => (value / 1000000).toFixed(2),
},
{
title: 'Error Rate (% of total)',
dataIndex: 'errorRate',
key: 'errorRate',
width: 150,
sorter: (a: DataProps, b: DataProps): number => a.errorRate - b.errorRate,
render: (value: number): string => value.toFixed(2),
},
{
title: 'Operations Per Second',
dataIndex: 'callRate',
key: 'callRate',
width: 150,
sorter: (a: DataProps, b: DataProps): number => a.callRate - b.callRate,
render: (value: number): string => value.toFixed(2),
},
],
[getColumnSearchProps],
);
if (
services.length === 0 &&
loading === false &&
!skipOnboarding &&
error === true
) {
return <SkipBoardModal onContinueClick={onContinueClick} />;
}
return (
<Container>
<ResizeTable
columns={columns}
loading={loading}
dataSource={services}
rowKey="serviceName"
/>
</Container>
);
}
type DataProps = ServicesList;
export default Metrics;

View File

@ -1,6 +1,4 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { initialQueriesMap } from 'constants/queryBuilder';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
@ -17,7 +15,7 @@ import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import DashboardReducer from 'types/reducer/dashboards';
import menuItems, { ITEMS } from './menuItems';
import menuItems from './menuItems';
import { Card, Container, Text } from './styles';
function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
@ -31,7 +29,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
const { data } = selectedDashboard;
const onClickHandler = useCallback(
async (name: ITEMS) => {
(name: PANEL_TYPES) => (): void => {
try {
const emptyLayout = data.layout?.find((e) => e.i === 'empty');
@ -65,14 +63,7 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
return (
<Container>
{menuItems.map(({ name, Icon, display }) => (
<Card
onClick={(event): void => {
event.preventDefault();
onClickHandler(name);
}}
id={name}
key={name}
>
<Card onClick={onClickHandler(name)} id={name} key={name}>
<Icon fillColor={fillColor} />
<Text>{display}</Text>
</Card>
@ -81,8 +72,6 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
);
}
export type GRAPH_TYPES = ITEMS;
interface DispatchProps {
toggleAddWidget: (
props: ToggleAddWidgetProps,

View File

@ -1,4 +1,5 @@
import TimeSeries from 'assets/Dashboard/TimeSeries';
import TableIcon from 'assets/Dashboard/Table';
import TimeSeriesIcon from 'assets/Dashboard/TimeSeries';
import ValueIcon from 'assets/Dashboard/Value';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { CSSProperties } from 'react';
@ -6,7 +7,7 @@ import { CSSProperties } from 'react';
const Items: ItemsProps[] = [
{
name: PANEL_TYPES.TIME_SERIES,
Icon: TimeSeries,
Icon: TimeSeriesIcon,
display: 'Time Series',
},
{
@ -14,18 +15,11 @@ const Items: ItemsProps[] = [
Icon: ValueIcon,
display: 'Value',
},
{ name: PANEL_TYPES.TABLE, Icon: TableIcon, display: 'Table' },
];
export type ITEMS =
| 'graph'
| 'value'
| 'list'
| 'table'
| 'EMPTY_WIDGET'
| 'trace';
interface ItemsProps {
name: ITEMS;
name: PANEL_TYPES;
Icon: (props: IconProps) => JSX.Element;
display: string;
}

View File

@ -16,6 +16,7 @@ export const Card = styled(CardComponent)`
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
`;

View File

@ -1,6 +1,6 @@
import { Button, Tabs, Typography } from 'antd';
import TextToolTip from 'components/TextToolTip';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { WidgetGraphProps } from 'container/NewWidget/types';
import { QueryBuilder } from 'container/QueryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
@ -170,7 +170,7 @@ const mapDispatchToProps = (
});
interface QueryProps extends DispatchProps {
selectedGraph: GRAPH_TYPES;
selectedGraph: PANEL_TYPES;
selectedTime: WidgetGraphProps['selectedTime'];
}

View File

@ -1,20 +1,23 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { EQueryType } from 'types/common/dashboard';
import QueryTypeTag from '../QueryTypeTag';
import { PlotTagWrapperStyled } from './styles';
interface IPlotTagProps {
queryType: EQueryType;
panelType: PANEL_TYPES;
}
function PlotTag({ queryType }: IPlotTagProps): JSX.Element | null {
function PlotTag({ queryType, panelType }: IPlotTagProps): JSX.Element | null {
if (queryType === undefined) {
return null;
}
return (
<div style={{ marginLeft: '2rem', position: 'absolute', top: '1rem' }}>
<PlotTagWrapperStyled $panelType={panelType}>
Plotted using <QueryTypeTag queryType={queryType} />
</div>
</PlotTagWrapperStyled>
);
}

View File

@ -1,8 +1,9 @@
import { Card, Typography } from 'antd';
import Spinner from 'components/Spinner';
import GridGraphComponent from 'container/GridGraphComponent';
import GridPanelSwitch from 'container/GridPanelSwitch';
import { WidgetGraphProps } from 'container/NewWidget/types';
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import getChartData from 'lib/getChartData';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
@ -16,6 +17,7 @@ function WidgetGraph({
yAxisUnit,
selectedTime,
}: WidgetGraphProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
const { dashboards } = useSelector<AppState, DashboardReducer>(
(state) => state.dashboards,
);
@ -39,7 +41,7 @@ function WidgetGraph({
return <Card>Invalid widget</Card>;
}
const { title, opacity, isStacked } = selectedWidget;
const { title, opacity, isStacked, query } = selectedWidget;
if (getWidgetQueryRange.error) {
return (
@ -66,14 +68,18 @@ function WidgetGraph({
});
return (
<GridGraphComponent
<GridPanelSwitch
title={title}
isStacked={isStacked}
opacity={opacity}
data={chartDataSet}
GRAPH_TYPES={selectedGraph}
panelType={selectedGraph}
name={widgetId || 'legend_widget'}
yAxisUnit={yAxisUnit}
panelData={
getWidgetQueryRange.data?.payload.data.newResult.data.result || []
}
query={stagedQuery || query}
/>
);
}

View File

@ -41,12 +41,12 @@ function WidgetGraph({
});
if (selectedWidget === undefined) {
return <Card>Invalid widget</Card>;
return <Card $panelType={selectedGraph}>Invalid widget</Card>;
}
return (
<Container>
<PlotTag queryType={currentQuery.queryType} />
<Container $panelType={selectedGraph}>
<PlotTag queryType={currentQuery.queryType} panelType={selectedGraph} />
{getWidgetQueryRange.error && (
<AlertIconContainer color="red" title={getWidgetQueryRange.error.message}>
<InfoCircleOutlined />

View File

@ -1,15 +1,23 @@
import { Card, Tooltip } from 'antd';
import { PANEL_TYPES } from 'constants/queryBuilder';
import styled from 'styled-components';
export const Container = styled(Card)`
interface Props {
$panelType: PANEL_TYPES;
}
export const Container = styled(Card)<Props>`
&&& {
position: relative;
}
.ant-card-body {
padding: 1.5rem 0;
padding: ${({ $panelType }): string =>
$panelType === PANEL_TYPES.TABLE ? '0 0' : '1.5rem 0'};
height: 57vh;
/* padding-bottom: 2rem; */
overflow: auto;
display: flex;
flex-direction: column;
}
`;
@ -23,5 +31,14 @@ export const NotFoundContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
min-height: 55vh;
min-height: 47vh;
`;
export const PlotTagWrapperStyled = styled.div<Props>`
margin-left: 2rem;
margin-top: ${({ $panelType }): string =>
$panelType === PANEL_TYPES.TABLE ? '1rem' : '0'};
margin-bottom: ${({ $panelType }): string =>
$panelType === PANEL_TYPES.TABLE ? '1rem' : '0'};
`;

View File

@ -1,10 +1,8 @@
import { Input, Select } from 'antd';
import InputComponent from 'components/Input';
import TimePreference from 'components/TimePreferenceDropDown';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import GraphTypes, {
ITEMS,
} from 'container/NewDashboard/ComponentsSlider/menuItems';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GraphTypes from 'container/NewDashboard/ComponentsSlider/menuItems';
import { Dispatch, SetStateAction, useCallback } from 'react';
import { Container, Title } from './styles';
@ -144,12 +142,12 @@ interface RightContainerProps {
setOpacity: Dispatch<SetStateAction<string>>;
selectedNullZeroValue: string;
setSelectedNullZeroValue: Dispatch<SetStateAction<string>>;
selectedGraph: GRAPH_TYPES;
selectedGraph: PANEL_TYPES;
setSelectedTime: Dispatch<SetStateAction<timePreferance>>;
selectedTime: timePreferance;
yAxisUnit: string;
setYAxisUnit: Dispatch<SetStateAction<string>>;
setGraphHandler: (type: ITEMS) => void;
setGraphHandler: (type: PANEL_TYPES) => void;
}
export default RightContainer;

View File

@ -1,8 +1,8 @@
import { LockFilled } from '@ant-design/icons';
import { Button, Modal, Tooltip, Typography } from 'antd';
import { FeatureKeys } from 'constants/features';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
import { useNotifications } from 'hooks/useNotifications';
@ -148,7 +148,7 @@ function NewWidget({ selectedGraph, saveSettingOfPanel }: Props): JSX.Element {
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, dispatch]);
const setGraphHandler = (type: ITEMS): void => {
const setGraphHandler = (type: PANEL_TYPES): void => {
const params = new URLSearchParams(search);
params.set('graphType', type);
history.push({ search: params.toString() });

View File

@ -1,10 +1,10 @@
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { Widgets } from 'types/api/dashboard/getAll';
import { timePreferance } from './RightContainer/timeItems';
export interface NewWidgetProps {
selectedGraph: GRAPH_TYPES;
selectedGraph: PANEL_TYPES;
yAxisUnit: Widgets['yAxisUnit'];
}

View File

@ -1,10 +1,18 @@
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { WhereClauseConfig } from 'hooks/queryBuilder/useAutoComplete';
import { ReactNode } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { OrderByFilterProps } from './filters/OrderByFilter/OrderByFilter.interfaces';
type FilterConfigs = {
[Key in keyof Omit<IBuilderQuery, 'filters'>]: {
isHidden: boolean;
isDisabled: boolean;
};
} & { filters: WhereClauseConfig };
export type QueryBuilderConfig =
| {
queryVariant: 'static';
@ -14,10 +22,8 @@ export type QueryBuilderConfig =
export type QueryBuilderProps = {
config?: QueryBuilderConfig;
panelType: ITEMS;
panelType: PANEL_TYPES;
actions?: ReactNode;
filterConfigs?: Partial<
Record<keyof IBuilderQuery, { isHidden: boolean; isDisabled: boolean }>
>;
filterConfigs?: Partial<FilterConfigs>;
queryComponents?: { renderOrderBy?: (props: OrderByFilterProps) => ReactNode };
};

View File

@ -220,16 +220,19 @@ export const Query = memo(function Query({
</Col>
</Row>
</Col>
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="HAVING" />
</Col>
<Col flex="1 1 12.5rem">
<HavingFilter onChange={handleChangeHavingFilter} query={query} />
</Col>
</Row>
</Col>
{!filterConfigs?.having?.isHidden && (
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
<FilterLabel label="HAVING" />
</Col>
<Col flex="1 1 12.5rem">
<HavingFilter onChange={handleChangeHavingFilter} query={query} />
</Col>
</Row>
</Col>
)}
<Col span={11}>
<Row gutter={[11, 5]}>
<Col flex="5.93rem">
@ -248,6 +251,7 @@ export const Query = memo(function Query({
panelType,
isMetricsDataSource,
query,
filterConfigs?.having?.isHidden,
handleChangeLimit,
handleChangeHavingFilter,
renderOrderByFilter,
@ -305,7 +309,11 @@ export const Query = memo(function Query({
</Col>
)}
<Col flex="1">
<QueryBuilderSearch query={query} onChange={handleChangeTagFilters} />
<QueryBuilderSearch
query={query}
onChange={handleChangeTagFilters}
whereClauseConfig={filterConfigs?.filters}
/>
</Col>
</Row>
</Col>

View File

@ -1,5 +1,8 @@
import { Select, Spin, Tag, Tooltip } from 'antd';
import { useAutoComplete } from 'hooks/queryBuilder/useAutoComplete';
import {
useAutoComplete,
WhereClauseConfig,
} from 'hooks/queryBuilder/useAutoComplete';
import { useFetchKeysAndValues } from 'hooks/queryBuilder/useFetchKeysAndValues';
import {
KeyboardEvent,
@ -31,6 +34,7 @@ import {
function QueryBuilderSearch({
query,
onChange,
whereClauseConfig,
}: QueryBuilderSearchProps): JSX.Element {
const {
updateTag,
@ -45,7 +49,7 @@ function QueryBuilderSearch({
isFetching,
setSearchKey,
searchKey,
} = useAutoComplete(query);
} = useAutoComplete(query, whereClauseConfig);
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
searchValue,
@ -169,7 +173,7 @@ function QueryBuilderSearch({
notFoundContent={isFetching ? <Spin size="small" /> : null}
>
{options.map((option) => (
<Select.Option key={option.label} value={option.label}>
<Select.Option key={option.label} value={option.value}>
{option.label}
{option.selected && <StyledCheckOutlined />}
</Select.Option>
@ -181,8 +185,13 @@ function QueryBuilderSearch({
interface QueryBuilderSearchProps {
query: IBuilderQuery;
onChange: (value: TagFilter) => void;
whereClauseConfig?: WhereClauseConfig;
}
QueryBuilderSearch.defaultProps = {
whereClauseConfig: undefined,
};
export interface CustomTagProps {
label: ReactNode;
value: string;

View File

@ -13,4 +13,5 @@ export type QueryTableProps = Omit<
query: Query;
renderActionCell?: (record: RowData) => ReactNode;
modifyColumns?: (columns: ColumnsType<RowData>) => ColumnsType<RowData>;
renderColumnCell?: Record<string, (record: RowData) => ReactNode>;
};

View File

@ -9,6 +9,7 @@ export function QueryTable({
query,
renderActionCell,
modifyColumns,
renderColumnCell,
...props
}: QueryTableProps): JSX.Element {
const { columns, dataSource } = useMemo(
@ -17,15 +18,12 @@ export function QueryTable({
query,
queryTableData,
renderActionCell,
renderColumnCell,
}),
[query, queryTableData, renderActionCell],
[query, queryTableData, renderColumnCell, renderActionCell],
);
const filteredColumns = columns.filter((item) => item.key !== 'timestamp');
const tableColumns = modifyColumns
? modifyColumns(filteredColumns)
: filteredColumns;
const tableColumns = modifyColumns ? modifyColumns(columns) : columns;
return (
<ResizeTable

View File

@ -0,0 +1,3 @@
import { CSSProperties } from 'styled-components';
export const QUERY_TABLE_CONFIG: CSSProperties = { width: 145 };

View File

@ -0,0 +1,26 @@
export enum ColumnKey {
Application = 'serviceName',
P99 = 'p99',
ErrorRate = 'errorRate',
Operations = 'callRate',
}
export const ColumnTitle: {
[key in ColumnKey]: string;
} = {
[ColumnKey.Application]: 'Application',
[ColumnKey.P99]: 'P99 latency (in ms)',
[ColumnKey.ErrorRate]: 'Error Rate (% of total)',
[ColumnKey.Operations]: 'Operations Per Second',
};
export enum ColumnWidth {
Application = 200,
P99 = 150,
ErrorRate = 150,
Operations = 150,
}
export const SORTING_ORDER = 'descend';
export const SEARCH_PLACEHOLDER = 'Search by service';

View File

@ -0,0 +1,34 @@
import { SearchOutlined } from '@ant-design/icons';
import type { ColumnType } from 'antd/es/table';
import ROUTES from 'constants/routes';
import { routeConfig } from 'container/SideNav/config';
import { getQueryString } from 'container/SideNav/helper';
import { Link } from 'react-router-dom';
import { ServicesList } from 'types/api/metrics/getService';
import { filterDropdown } from '../Filter/FilterDropdown';
import { Name } from '../styles';
export const getColumnSearchProps = (
dataIndex: keyof ServicesList,
search: string,
): ColumnType<ServicesList> => ({
filterDropdown,
filterIcon: <SearchOutlined />,
onFilter: (value: string | number | boolean, record: ServicesList): boolean =>
record[dataIndex]
.toString()
.toLowerCase()
.includes(value.toString().toLowerCase()),
render: (metrics: string): JSX.Element => {
const urlParams = new URLSearchParams(search);
const avialableParams = routeConfig[ROUTES.SERVICE_METRICS];
const queryString = getQueryString(avialableParams, urlParams);
return (
<Link to={`${ROUTES.APPLICATION}/${metrics}?${queryString.join('')}`}>
<Name>{metrics}</Name>
</Link>
);
},
});

View File

@ -0,0 +1,46 @@
import type { ColumnsType } from 'antd/es/table';
import { ServicesList } from 'types/api/metrics/getService';
import {
ColumnKey,
ColumnTitle,
ColumnWidth,
SORTING_ORDER,
} from './ColumnContants';
import { getColumnSearchProps } from './GetColumnSearchProps';
export const getColumns = (search: string): ColumnsType<ServicesList> => [
{
title: ColumnTitle[ColumnKey.Application],
dataIndex: ColumnKey.Application,
width: ColumnWidth.Application,
key: ColumnKey.Application,
...getColumnSearchProps('serviceName', search),
},
{
title: ColumnTitle[ColumnKey.P99],
dataIndex: ColumnKey.P99,
key: ColumnKey.P99,
width: ColumnWidth.P99,
defaultSortOrder: SORTING_ORDER,
sorter: (a: ServicesList, b: ServicesList): number => a.p99 - b.p99,
render: (value: number): string => (value / 1000000).toFixed(2),
},
{
title: ColumnTitle[ColumnKey.ErrorRate],
dataIndex: ColumnKey.ErrorRate,
key: ColumnKey.ErrorRate,
width: 150,
sorter: (a: ServicesList, b: ServicesList): number =>
a.errorRate - b.errorRate,
render: (value: number): string => value.toFixed(2),
},
{
title: ColumnTitle[ColumnKey.Operations],
dataIndex: ColumnKey.Operations,
key: ColumnKey.Operations,
width: ColumnWidth.Operations,
sorter: (a: ServicesList, b: ServicesList): number => a.callRate - b.callRate,
render: (value: number): string => value.toFixed(2),
},
];

View File

@ -0,0 +1,41 @@
import { SearchOutlined } from '@ant-design/icons';
import { Button, Card, Input, Space } from 'antd';
import type { FilterDropdownProps } from 'antd/es/table/interface';
import { SEARCH_PLACEHOLDER } from '../Columns/ColumnContants';
export const filterDropdown = ({
setSelectedKeys,
selectedKeys,
confirm,
}: FilterDropdownProps): JSX.Element => {
const handleSearch = (): void => {
confirm();
};
const selectedKeysHandler = (e: React.ChangeEvent<HTMLInputElement>): void => {
setSelectedKeys(e.target.value ? [e.target.value] : []);
};
return (
<Card size="small">
<Space align="start" direction="vertical">
<Input
placeholder={SEARCH_PLACEHOLDER}
value={selectedKeys[0]}
onChange={selectedKeysHandler}
allowClear
onPressEnter={handleSearch}
/>
<Button
type="primary"
onClick={handleSearch}
icon={<SearchOutlined />}
size="small"
>
Search
</Button>
</Space>
</Card>
);
};

View File

@ -0,0 +1,50 @@
import { render, screen, waitFor } from '@testing-library/react';
import ROUTES from 'constants/routes';
import { BrowserRouter } from 'react-router-dom';
import { Services } from './__mock__/servicesListMock';
import Metrics from './index';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: (): { pathname: string } => ({
pathname: `${process.env.FRONTEND_API_ENDPOINT}/${ROUTES.APPLICATION}/`,
}),
}));
describe('Metrics Component', () => {
it('renders without errors', async () => {
render(
<BrowserRouter>
<Metrics services={Services} isLoading={false} />
</BrowserRouter>,
);
await waitFor(() => {
expect(screen.getByText(/application/i)).toBeInTheDocument();
expect(screen.getByText(/p99 latency \(in ms\)/i)).toBeInTheDocument();
expect(screen.getByText(/error rate \(% of total\)/i)).toBeInTheDocument();
expect(screen.getByText(/operations per second/i)).toBeInTheDocument();
});
});
it('renders if the data is loaded in the table', async () => {
render(
<BrowserRouter>
<Metrics services={Services} isLoading={false} />
</BrowserRouter>,
);
expect(screen.getByText('frontend')).toBeInTheDocument();
});
it('renders no data when required conditions are met', async () => {
render(
<BrowserRouter>
<Metrics services={[]} isLoading={false} />
</BrowserRouter>,
);
expect(screen.getByText('No data')).toBeInTheDocument();
});
});

View File

@ -0,0 +1,22 @@
import { ServicesList } from 'types/api/metrics/getService';
export const Services: ServicesList[] = [
{
serviceName: 'frontend',
p99: 1261498140,
avgDuration: 768497850.9803921,
numCalls: 255,
callRate: 0.9444444444444444,
numErrors: 0,
errorRate: 0,
},
{
serviceName: 'customer',
p99: 890150740.0000001,
avgDuration: 369612035.2941176,
numCalls: 255,
callRate: 0.9444444444444444,
numErrors: 0,
errorRate: 0,
},
];

View File

@ -0,0 +1,26 @@
import { ResizeTable } from 'components/ResizeTable';
import { useMemo } from 'react';
import { useLocation } from 'react-router-dom';
import { getColumns } from './Columns/ServiceColumn';
import { Container } from './styles';
import ServiceTableProp from './types';
function Services({ services, isLoading }: ServiceTableProp): JSX.Element {
const { search } = useLocation();
const tableColumns = useMemo(() => getColumns(search), [search]);
return (
<Container>
<ResizeTable
columns={tableColumns}
dataSource={services}
loading={isLoading}
rowKey="serviceName"
/>
</Container>
);
}
export default Services;

View File

@ -0,0 +1,6 @@
import { ServicesList } from 'types/api/metrics/getService';
export default interface ServiceTableProp {
services: ServicesList[];
isLoading: boolean;
}

View File

@ -17,12 +17,14 @@ function QuerySection(): JSX.Element {
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const isList = panelTypes === PANEL_TYPES.LIST;
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: false, isDisabled: true },
having: { isHidden: isList, isDisabled: true },
};
return config;
}, []);
}, [panelTypes]);
const renderOrderBy = useCallback(
({ query, onChange }: OrderByFilterProps) => (

View File

@ -1,7 +1,6 @@
import {
getRemovePrefixFromKey,
getTagToken,
isExistsNotExistsOperator,
replaceStringWithMaxLength,
tagRegexp,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
@ -16,7 +15,15 @@ import { useSetCurrentKeyAndOperator } from './useSetCurrentKeyAndOperator';
import { useTag } from './useTag';
import { useTagValidation } from './useTagValidation';
export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
export type WhereClauseConfig = {
customKey: string;
customOp: string;
};
export const useAutoComplete = (
query: IBuilderQuery,
whereClauseConfig?: WhereClauseConfig,
): IAutoComplete => {
const [searchValue, setSearchValue] = useState<string>('');
const [searchKey, setSearchKey] = useState<string>('');
@ -40,11 +47,11 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
);
const { handleAddTag, handleClearTag, tags, updateTag } = useTag(
key,
isValidTag,
handleSearch,
query,
setSearchKey,
whereClauseConfig,
);
const handleSelect = useCallback(
@ -59,11 +66,10 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
});
}
if (!isMulti) {
if (isExistsNotExistsOperator(value)) handleAddTag(value);
if (isValidTag && !isExistsNotExistsOperator(value)) handleAddTag(value);
handleAddTag(value);
}
},
[handleAddTag, isMulti, isValidTag],
[handleAddTag, isMulti],
);
const handleKeyDown = useCallback(
@ -102,6 +108,7 @@ export const useAutoComplete = (query: IBuilderQuery): IAutoComplete => {
isExist,
results,
result,
whereClauseConfig,
);
return {

View File

@ -1,6 +1,5 @@
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { useMemo } from 'react';
import { UseQueryOptions, UseQueryResult } from 'react-query';
import { useSelector } from 'react-redux';
@ -15,7 +14,7 @@ import { useQueryBuilder } from './useQueryBuilder';
export const useGetExplorerQueryRange = (
requestData: Query | null,
panelType: GRAPH_TYPES | null,
panelType: PANEL_TYPES | null,
options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
): UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error> => {
const { isEnabledQuery } = useQueryBuilder();

View File

@ -1,11 +1,11 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import useUrlQuery from 'hooks/useUrlQuery';
import { useMemo } from 'react';
export const useGetPanelTypesQueryParam = <T extends GRAPH_TYPES | undefined>(
export const useGetPanelTypesQueryParam = <T extends PANEL_TYPES | undefined>(
defaultPanelType?: T,
): T extends undefined ? GRAPH_TYPES | null : GRAPH_TYPES => {
): T extends undefined ? PANEL_TYPES | null : PANEL_TYPES => {
const urlQuery = useUrlQuery();
return useMemo(() => {

View File

@ -7,8 +7,11 @@ import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { WhereClauseConfig } from './useAutoComplete';
import { useOperators } from './useOperators';
export const WHERE_CLAUSE_CUSTOM_SUFFIX = '-custom';
export const useOptions = (
key: string,
keys: BaseAutocompleteData[],
@ -19,6 +22,7 @@ export const useOptions = (
isExist: boolean,
results: string[],
result: string[],
whereClauseConfig?: WhereClauseConfig,
): Option[] => {
const [options, setOptions] = useState<Option[]>([]);
const operators = useOperators(key, keys);
@ -51,21 +55,64 @@ export const useOptions = (
[key, operator],
);
const getOptionsWithValidOperator = useCallback(
(key: string, results: string[], searchValue: string) => {
const hasAllResults = results.every((value) => result.includes(value));
const values = getKeyOpValue(results);
return hasAllResults
? [
{
label: searchValue,
value: searchValue,
},
]
: [
{
label: searchValue,
value: searchValue,
},
...values,
];
},
[getKeyOpValue, result],
);
const getKeyOperatorOptions = useCallback(
(key: string) => {
const operatorsOptions = operators?.map((operator) => ({
value: `${key} ${operator} `,
label: `${key} ${operator} `,
}));
if (whereClauseConfig) {
return [
{
label: `${searchValue} `,
value: `${searchValue}${WHERE_CLAUSE_CUSTOM_SUFFIX}`,
},
...operatorsOptions,
];
}
return operatorsOptions;
},
[operators, searchValue, whereClauseConfig],
);
useEffect(() => {
let newOptions: Option[] = [];
if (!key) {
newOptions = searchValue
? [
{ label: `${searchValue} `, value: `${searchValue} ` },
{
label: `${searchValue} `,
value: `${searchValue} `,
},
...getOptionsFromKeys(keys),
]
: getOptionsFromKeys(keys);
} else if (key && !operator) {
newOptions = operators?.map((operator) => ({
value: `${key} ${operator} `,
label: `${key} ${operator} `,
}));
newOptions = getKeyOperatorOptions(key);
} else if (key && operator) {
if (isMulti) {
newOptions = results.map((item) => ({
@ -75,17 +122,14 @@ export const useOptions = (
} else if (isExist) {
newOptions = [];
} else if (isValidOperator) {
const hasAllResults = results.every((value) => result.includes(value));
const values = getKeyOpValue(results);
newOptions = hasAllResults
? [{ label: searchValue, value: searchValue }]
: [{ label: searchValue, value: searchValue }, ...values];
newOptions = getOptionsWithValidOperator(key, results, searchValue);
}
}
if (newOptions.length > 0) {
setOptions(newOptions);
}
}, [
whereClauseConfig,
getKeyOpValue,
getOptionsFromKeys,
isExist,
@ -98,6 +142,8 @@ export const useOptions = (
result,
results,
searchValue,
getKeyOperatorOptions,
getOptionsWithValidOperator,
]);
return useMemo(

View File

@ -63,9 +63,18 @@ export const useQueryOperations: UseQueryOperations = ({
const getNewListOfAdditionalFilters = useCallback(
(dataSource: DataSource): string[] => {
const additionalFiltersKeys: (keyof Pick<
IBuilderQuery,
'orderBy' | 'limit' | 'having' | 'stepInterval'
>)[] = ['having', 'limit', 'orderBy', 'stepInterval'];
const result: string[] = mapOfFilters[dataSource].reduce<string[]>(
(acc, item) => {
if (filterConfigs && filterConfigs[item.field]?.isHidden) {
if (
filterConfigs &&
filterConfigs[item.field as typeof additionalFiltersKeys[number]]
?.isHidden
) {
return acc;
}

View File

@ -15,17 +15,14 @@ export const useSetCurrentKeyAndOperator = (
let key = '';
let operator = '';
let result: string[] = [];
if (value) {
const { tagKey, tagOperator, tagValue } = getTagToken(value);
const isSuggestKey = keys?.some(
(el) => el?.key === getRemovePrefixFromKey(tagKey),
);
if (isSuggestKey || keys.length === 0) {
key = tagKey || '';
operator = tagOperator || '';
result = tagValue || [];
}
const { tagKey, tagOperator, tagValue } = getTagToken(value);
const isSuggestKey = keys?.some(
(el) => el?.key === getRemovePrefixFromKey(tagKey),
);
if (isSuggestKey || keys.length === 0) {
key = tagKey || '';
operator = tagOperator || '';
result = tagValue || [];
}
return [key, operator, result];

View File

@ -1,5 +1,6 @@
import {
getOperatorFromValue,
getTagToken,
isExistsNotExistsOperator,
isInNInOperator,
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
@ -8,6 +9,8 @@ import * as Papa from 'papaparse';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { WhereClauseConfig } from './useAutoComplete';
type IUseTag = {
handleAddTag: (value: string) => void;
handleClearTag: (value: string) => void;
@ -24,11 +27,11 @@ type IUseTag = {
*/
export const useTag = (
key: string,
isValidTag: boolean,
handleSearch: (value: string) => void,
query: IBuilderQuery,
setSearchKey: (value: string) => void,
whereClauseConfig?: WhereClauseConfig,
): IUseTag => {
const initTagsData = useMemo(
() =>
@ -57,15 +60,31 @@ export const useTag = (
* Adds a new tag to the tag list.
* @param {string} value - The tag value to be added.
*/
const handleAddTag = useCallback(
(value: string): void => {
const { tagKey } = getTagToken(value);
const [key, id] = tagKey.split('-');
if (id === 'custom') {
const customValue = whereClauseConfig
? `${whereClauseConfig.customKey} ${whereClauseConfig.customOp} ${key}`
: '';
setTags((prevTags) =>
prevTags.includes(customValue) ? prevTags : [...prevTags, customValue],
);
handleSearch('');
setSearchKey('');
return;
}
if ((value && key && isValidTag) || isExistsNotExistsOperator(value)) {
setTags((prevTags) => [...prevTags, value]);
handleSearch('');
setSearchKey('');
}
},
[key, isValidTag, handleSearch, setSearchKey],
[whereClauseConfig, isValidTag, handleSearch, setSearchKey],
);
/**

View File

@ -0,0 +1,17 @@
import { AxiosError } from 'axios';
import { useEffect } from 'react';
import { useNotifications } from './useNotifications';
const useErrorNotification = (error: AxiosError | null): void => {
const { notifications } = useNotifications();
useEffect(() => {
if (error) {
notifications.error({
message: error.message,
});
}
}, [error, notifications]);
};
export default useErrorNotification;

View File

@ -0,0 +1,29 @@
import getService from 'api/metrics/getService';
import { AxiosError } from 'axios';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import { useQuery, UseQueryResult } from 'react-query';
import { PayloadProps } from 'types/api/metrics/getService';
import { Tags } from 'types/reducer/trace';
export const useQueryService = ({
minTime,
maxTime,
selectedTime,
selectedTags,
}: UseQueryServiceProps): UseQueryResult<PayloadProps, AxiosError> => {
const queryKey = [minTime, maxTime, selectedTime, selectedTags];
return useQuery<PayloadProps, AxiosError>(queryKey, () =>
getService({
end: maxTime,
start: minTime,
selectedTags,
}),
);
};
interface UseQueryServiceProps {
minTime: number;
maxTime: number;
selectedTime: Time;
selectedTags: Tags[];
}

View File

@ -1,4 +1,4 @@
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
import { Time } from 'container/TopNav/DateTimeSelection/config';
import store from 'store';
@ -36,7 +36,7 @@ const getStartEndRangeTime = ({
interface GetStartEndRangeTimesProps {
type?: timePreferenceType;
graphType?: ITEMS | null;
graphType?: PANEL_TYPES | null;
interval?: Time;
}

View File

@ -1,11 +1,14 @@
import { mapOfOperators, PANEL_TYPES } from 'constants/queryBuilder';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
import {
DataSource,
MetricAggregateOperator,
StringOperators,
} from 'types/common/queryBuilder';
import { SelectOption } from 'types/common/select';
type GetQueryOperatorsParams = {
dataSource: DataSource;
panelType: GRAPH_TYPES;
panelType: PANEL_TYPES;
};
// Modify this function if need special conditions for filtering of the operators
@ -20,6 +23,13 @@ export const getOperatorsBySourceAndPanelType = ({
(operator) => operator.value === StringOperators.NOOP,
);
}
if (panelType === PANEL_TYPES.TABLE && dataSource === DataSource.METRICS) {
operatorsByDataSource = operatorsByDataSource.filter(
(operator) =>
operator.value !== MetricAggregateOperator.NOOP &&
operator.value !== MetricAggregateOperator.RATE,
);
}
if (
dataSource !== DataSource.METRICS &&
panelType !== PANEL_TYPES.LIST &&

View File

@ -1,8 +1,12 @@
import { ColumnsType } from 'antd/es/table';
import { ColumnType } from 'antd/lib/table';
import {
initialFormulaBuilderFormValues,
initialQueryBuilderFormValues,
} from 'constants/queryBuilder';
import { FORMULA_REGEXP } from 'constants/regExp';
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
import { toCapitalize } from 'lib/toCapitalize';
import { ReactNode } from 'react';
import {
IBuilderFormula,
@ -15,7 +19,7 @@ import { v4 as uuid } from 'uuid';
type CreateTableDataFromQueryParams = Pick<
QueryTableProps,
'queryTableData' | 'query' | 'renderActionCell'
'queryTableData' | 'query' | 'renderActionCell' | 'renderColumnCell'
>;
export type RowData = {
@ -25,9 +29,10 @@ export type RowData = {
};
type DynamicColumn = {
key: keyof RowData;
query: IBuilderQuery | IBuilderFormula;
field: string;
dataIndex: string;
title: string;
sourceLabel: string;
data: (string | number)[];
type: 'field' | 'operator' | 'formula';
// sortable: boolean;
@ -55,7 +60,6 @@ type GetDynamicColumns = (
type ListItemData = ListItem['data'];
type ListItemKey = keyof ListItemData;
type SeriesItemLabels = SeriesItem['labels'];
const isFormula = (queryName: string): boolean =>
FORMULA_REGEXP.test(queryName);
@ -74,32 +78,31 @@ const getQueryByName = <T extends keyof QueryBuilderData>(
builder: QueryBuilderData,
currentQueryName: string,
type: T,
): (T extends 'queryData' ? IBuilderQuery : IBuilderFormula) | null => {
): T extends 'queryData' ? IBuilderQuery : IBuilderFormula => {
const queryArray = builder[type];
const defaultValue =
type === 'queryData'
? initialQueryBuilderFormValues
: initialFormulaBuilderFormValues;
const currentQuery =
queryArray.find((q) => q.queryName === currentQueryName) || null;
if (!currentQuery) return null;
queryArray.find((q) => q.queryName === currentQueryName) || defaultValue;
return currentQuery as T extends 'queryData' ? IBuilderQuery : IBuilderFormula;
};
const createLabels = <T extends ListItemData | SeriesItemLabels>(
// labels: T,
label: keyof T,
const addListLabels = (
query: IBuilderQuery | IBuilderFormula,
label: ListItemKey,
dynamicColumns: DynamicColumns,
): void => {
if (isValueExist('key', label as string, dynamicColumns)) return;
// const labelValue = labels[label];
// const isNumber = !Number.isNaN(parseFloat(String(labelValue)));
if (isValueExist('dataIndex', label, dynamicColumns)) return;
const fieldObj: DynamicColumn = {
key: label as string,
query,
field: 'label',
dataIndex: label as string,
title: label as string,
sourceLabel: label as string,
data: [],
type: 'field',
// sortable: isNumber,
@ -108,42 +111,59 @@ const createLabels = <T extends ListItemData | SeriesItemLabels>(
dynamicColumns.push(fieldObj);
};
const appendOperatorFormulaColumns = (
builder: QueryBuilderData,
currentQueryName: string,
const addSeriaLabels = (
label: string,
dynamicColumns: DynamicColumns,
query: IBuilderQuery | IBuilderFormula,
): void => {
const currentFormula = getQueryByName(
builder,
currentQueryName,
'queryFormulas',
);
if (currentFormula) {
let formulaLabel = `${currentFormula.queryName}(${currentFormula.expression})`;
if (isValueExist('dataIndex', label, dynamicColumns)) return;
if (currentFormula.legend) {
formulaLabel += ` - ${currentFormula.legend}`;
// const labelValue = labels[label];
// const isNumber = !Number.isNaN(parseFloat(String(labelValue)));
const fieldObj: DynamicColumn = {
query,
field: label as string,
dataIndex: label,
title: label,
data: [],
type: 'field',
// sortable: isNumber,
};
dynamicColumns.push(fieldObj);
};
const addOperatorFormulaColumns = (
query: IBuilderFormula | IBuilderQuery,
dynamicColumns: DynamicColumns,
customLabel?: string,
): void => {
if (isFormula(query.queryName)) {
const formulaQuery = query as IBuilderFormula;
let formulaLabel = `${formulaQuery.queryName}(${formulaQuery.expression})`;
if (formulaQuery.legend) {
formulaLabel = formulaQuery.legend;
}
const formulaColumn: DynamicColumn = {
key: currentQueryName,
title: formulaLabel,
sourceLabel: formulaLabel,
query,
field: formulaQuery.queryName,
dataIndex: formulaQuery.queryName,
title: customLabel || formulaLabel,
data: [],
type: 'formula',
// sortable: isNumber,
};
dynamicColumns.push(formulaColumn);
return;
}
const currentQueryData = getQueryByName(
builder,
currentQueryName,
'queryData',
);
if (!currentQueryData) return;
const currentQueryData = query as IBuilderQuery;
let operatorLabel = `${currentQueryData.aggregateOperator}`;
if (currentQueryData.aggregateAttribute.key) {
@ -151,17 +171,14 @@ const appendOperatorFormulaColumns = (
}
if (currentQueryData.legend) {
operatorLabel += ` - ${currentQueryData.legend}`;
} else {
operatorLabel += ` - ${currentQueryData.queryName}`;
operatorLabel = currentQueryData.legend;
}
const resultValue = `${toCapitalize(operatorLabel)}`;
const operatorColumn: DynamicColumn = {
key: currentQueryName,
title: resultValue,
sourceLabel: resultValue,
query,
field: currentQueryData.queryName,
dataIndex: currentQueryData.queryName,
title: customLabel || operatorLabel,
data: [],
type: 'operator',
// sortable: isNumber,
@ -170,59 +187,71 @@ const appendOperatorFormulaColumns = (
dynamicColumns.push(operatorColumn);
};
const getDynamicColumns: GetDynamicColumns = (queryTableData, query) => {
const dynamicColumns: DynamicColumns = [];
queryTableData.forEach((currentQuery) => {
if (currentQuery.list) {
currentQuery.list.forEach((listItem) => {
Object.keys(listItem.data).forEach((label) => {
createLabels<ListItemData>(label as ListItemKey, dynamicColumns);
});
});
}
if (currentQuery.series) {
if (!isValueExist('key', 'timestamp', dynamicColumns)) {
dynamicColumns.push({
key: 'timestamp',
title: 'Timestamp',
sourceLabel: 'Timestamp',
data: [],
type: 'field',
// sortable: true,
});
}
appendOperatorFormulaColumns(
query.builder,
currentQuery.queryName,
dynamicColumns,
);
currentQuery.series.forEach((seria) => {
Object.keys(seria.labels).forEach((label) => {
createLabels<SeriesItemLabels>(label, dynamicColumns);
});
});
}
});
return dynamicColumns.map((item) => {
if (isFormula(item.key as string)) {
const transformColumnTitles = (
dynamicColumns: DynamicColumns,
): DynamicColumns =>
dynamicColumns.map((item) => {
if (isFormula(item.field as string)) {
return item;
}
const sameValues = dynamicColumns.filter(
(column) => column.sourceLabel === item.sourceLabel,
(column) => column.title === item.title,
);
if (sameValues.length > 1) {
return { ...item, title: `${item.title} - ${item.key}` };
return {
...item,
dataIndex: `${item.title} - ${item.query.queryName}`,
title: `${item.title} - ${item.query.queryName}`,
};
}
return item;
});
const getDynamicColumns: GetDynamicColumns = (queryTableData, query) => {
const dynamicColumns: DynamicColumns = [];
queryTableData.forEach((currentQuery) => {
const { series, queryName, list } = currentQuery;
const currentStagedQuery = getQueryByName(
query.builder,
queryName,
isFormula(queryName) ? 'queryFormulas' : 'queryData',
);
if (list) {
list.forEach((listItem) => {
Object.keys(listItem.data).forEach((label) => {
addListLabels(currentStagedQuery, label as ListItemKey, dynamicColumns);
});
});
}
if (series) {
const isValuesColumnExist = series.some((item) => item.values.length > 0);
const isEveryValuesExist = series.every((item) => item.values.length > 0);
if (isValuesColumnExist) {
addOperatorFormulaColumns(
currentStagedQuery,
dynamicColumns,
isEveryValuesExist ? undefined : currentStagedQuery.queryName,
);
}
series.forEach((seria) => {
Object.keys(seria.labels).forEach((label) => {
if (label === currentQuery?.queryName) return;
addSeriaLabels(label as string, dynamicColumns, currentStagedQuery);
});
});
}
});
return transformColumnTitles(dynamicColumns);
};
const fillEmptyRowCells = (
@ -231,8 +260,8 @@ const fillEmptyRowCells = (
currentColumn: DynamicColumn,
): void => {
unusedColumnsKeys.forEach((key) => {
if (key === currentColumn.key) {
const unusedCol = sourceColumns.find((item) => item.key === key);
if (key === currentColumn.field) {
const unusedCol = sourceColumns.find((item) => item.field === key);
if (unusedCol) {
unusedCol.data.push('N/A');
@ -242,33 +271,98 @@ const fillEmptyRowCells = (
});
};
const fillDataFromSeria = (
seria: SeriesItem,
columns: DynamicColumns,
queryName: string,
): void => {
const labelEntries = Object.entries(seria.labels);
const findSeriaValueFromAnotherQuery = (
currentLabels: Record<string, string>,
nextQuery: QueryDataV3 | null,
): SeriesItem | null => {
if (!nextQuery || !nextQuery.series) return null;
let value = null;
const labelEntries = Object.entries(currentLabels);
nextQuery.series.forEach((seria) => {
const localLabelEntries = Object.entries(seria.labels);
if (localLabelEntries.length !== labelEntries.length) return;
const isExistLabels = localLabelEntries.find(([key, value]) =>
labelEntries.find(
([currentKey, currentValue]) =>
currentKey === key && currentValue === value,
),
);
if (isExistLabels) {
value = seria;
}
});
return value;
};
const isEqualQueriesByLabel = (
equalQueries: string[],
queryName: string,
): boolean => equalQueries.includes(queryName);
const fillDataFromSeries = (
currentQuery: QueryDataV3,
queryTableData: QueryDataV3[],
columns: DynamicColumns,
equalQueriesByLabels: string[],
// TODO: fix it
// eslint-disable-next-line sonarjs/cognitive-complexity
): void => {
const { series, queryName } = currentQuery;
const isEqualQuery = isEqualQueriesByLabel(equalQueriesByLabels, queryName);
if (!series) return;
series.forEach((seria) => {
const labelEntries = Object.entries(seria.labels);
seria.values.forEach((value) => {
const unusedColumnsKeys = new Set<keyof RowData>(
columns.map((item) => item.key),
columns.map((item) => item.field),
);
columns.forEach((column) => {
if (column.key === 'timestamp') {
column.data.push(value.timestamp);
unusedColumnsKeys.delete('timestamp');
if (queryName === column.field) {
if (seria.values.length === 0) return;
column.data.push(parseFloat(seria.values[0].value).toFixed(2));
unusedColumnsKeys.delete(column.field);
return;
}
if (queryName === column.key) {
column.data.push(parseFloat(value.value).toFixed(2));
unusedColumnsKeys.delete(column.key);
if (column.type !== 'field' && column.field !== queryName) {
const nextQueryData =
queryTableData.find((q) => q.queryName === column.field) || null;
const targetSeria = findSeriaValueFromAnotherQuery(
seria.labels,
nextQueryData,
);
if (targetSeria) {
const isEqual = isEqualQueriesByLabel(equalQueriesByLabels, column.field);
if (!isEqual) {
equalQueriesByLabels.push(column.field);
}
column.data.push(parseFloat(targetSeria.values[0].value).toFixed(2));
} else {
column.data.push('N/A');
}
unusedColumnsKeys.delete(column.field);
return;
}
if (isEqualQuery) return;
labelEntries.forEach(([key, currentValue]) => {
if (column.key === key) {
if (column.field === key) {
column.data.push(currentValue);
unusedColumnsKeys.delete(key);
}
@ -284,10 +378,10 @@ const fillDataFromList = (
columns: DynamicColumns,
): void => {
columns.forEach((column) => {
if (isFormula(column.key as string)) return;
if (isFormula(column.field)) return;
Object.keys(listItem.data).forEach((label) => {
if (column.key === label) {
if (column.dataIndex === label) {
if (listItem.data[label as ListItemKey] !== '') {
column.data.push(listItem.data[label as ListItemKey].toString());
} else {
@ -304,15 +398,20 @@ const fillColumnsData: FillColumnData = (queryTableData, cols) => {
const formulas = cols.filter((item) => item.type === 'formula');
const resultColumns = [...fields, ...operators, ...formulas];
queryTableData.forEach((currentQuery) => {
if (currentQuery.series) {
currentQuery.series.forEach((seria) => {
fillDataFromSeria(seria, resultColumns, currentQuery.queryName);
});
}
const equalQueriesByLabels: string[] = [];
if (currentQuery.list) {
currentQuery.list.forEach((listItem) => {
queryTableData.forEach((currentQuery) => {
const { list } = currentQuery;
fillDataFromSeries(
currentQuery,
queryTableData,
resultColumns,
equalQueriesByLabels,
);
if (list) {
list.forEach((listItem) => {
fillDataFromList(listItem, resultColumns);
});
}
@ -331,9 +430,9 @@ const generateData = (
for (let i = 0; i < rowsLength; i += 1) {
const rowData: RowData = dynamicColumns.reduce((acc, item) => {
const { key } = item;
const { dataIndex } = item;
acc[key] = item.data[i];
acc[dataIndex] = item.data[i];
acc.key = uuid();
return acc;
@ -347,14 +446,16 @@ const generateData = (
const generateTableColumns = (
dynamicColumns: DynamicColumns,
renderColumnCell?: QueryTableProps['renderColumnCell'],
): ColumnsType<RowData> => {
const columns: ColumnsType<RowData> = dynamicColumns.reduce<
ColumnsType<RowData>
>((acc, item) => {
const column: ColumnType<RowData> = {
dataIndex: item.key,
key: item.key,
dataIndex: item.dataIndex,
title: item.title,
width: QUERY_TABLE_CONFIG.width,
render: renderColumnCell && renderColumnCell[item.dataIndex],
// sorter: item.sortable
// ? (a: RowData, b: RowData): number =>
// (a[item.key] as number) - (b[item.key] as number)
@ -371,6 +472,7 @@ export const createTableColumnsFromQuery: CreateTableDataFromQuery = ({
query,
queryTableData,
renderActionCell,
renderColumnCell,
}) => {
const dynamicColumns = getDynamicColumns(queryTableData, query);
@ -381,7 +483,7 @@ export const createTableColumnsFromQuery: CreateTableDataFromQuery = ({
const dataSource = generateData(filledDynamicColumns, rowsLength);
const columns = generateTableColumns(filledDynamicColumns);
const columns = generateTableColumns(filledDynamicColumns, renderColumnCell);
const actionsCell: ColumnType<RowData> | null = renderActionCell
? {

View File

@ -1,7 +1,7 @@
import { Card, Typography } from 'antd';
import Spinner from 'components/Spinner';
import { PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import NewWidget from 'container/NewWidget';
import history from 'lib/history';
import { useEffect, useRef, useState } from 'react';
@ -18,7 +18,7 @@ function DashboardWidget({ getDashboard }: NewDashboardProps): JSX.Element {
const { search } = useLocation();
const { dashboardId } = useParams<DashboardWidgetPageParams>();
const [selectedGraph, setSelectedGraph] = useState<GRAPH_TYPES>();
const [selectedGraph, setSelectedGraph] = useState<PANEL_TYPES>();
const { loading, dashboards, error, errorMessage } = useSelector<
AppState,
DashboardReducer
@ -34,7 +34,7 @@ function DashboardWidget({ getDashboard }: NewDashboardProps): JSX.Element {
useEffect(() => {
const params = new URLSearchParams(search);
const graphType = params.get('graphType') as GRAPH_TYPES | null;
const graphType = params.get('graphType') as PANEL_TYPES | null;
if (graphType === null) {
history.push(generatePath(ROUTES.DASHBOARD, { dashboardId }));

View File

@ -1,116 +0,0 @@
import { Space } from 'antd';
import getLocalStorageKey from 'api/browser/localstorage/get';
import ReleaseNote from 'components/ReleaseNote';
import Spinner from 'components/Spinner';
import { SKIP_ONBOARDING } from 'constants/onboarding';
import MetricTable from 'container/MetricsTable';
import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
import { useNotifications } from 'hooks/useNotifications';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useEffect, useMemo } from 'react';
import { connect, useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GetService, GetServiceProps } from 'store/actions/metrics';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { GlobalReducer } from 'types/reducer/globalTime';
import MetricReducer from 'types/reducer/metrics';
import { Tags } from 'types/reducer/trace';
function Metrics({ getService }: MetricsProps): JSX.Element {
const { minTime, maxTime, loading, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const location = useLocation();
const { services, error, errorMessage } = useSelector<AppState, MetricReducer>(
(state) => state.metrics,
);
const { notifications } = useNotifications();
useEffect(() => {
if (error) {
notifications.error({
message: errorMessage,
});
}
}, [error, errorMessage, notifications]);
const { queries } = useResourceAttribute();
const selectedTags = useMemo(
() => (convertRawQueriesToTraceSelectedTags(queries, '') as Tags[]) || [],
[queries],
);
const isSkipped = getLocalStorageKey(SKIP_ONBOARDING) === 'true';
useEffect(() => {
if (loading === false) {
getService({
maxTime,
minTime,
selectedTags,
});
}
}, [getService, loading, maxTime, minTime, selectedTags]);
useEffect(() => {
let timeInterval: NodeJS.Timeout;
if (loading === false && !isSkipped && services.length === 0) {
timeInterval = setInterval(() => {
getService({
maxTime,
minTime,
selectedTags,
});
}, 50000);
}
return (): void => {
clearInterval(timeInterval);
};
}, [
getService,
isSkipped,
loading,
maxTime,
minTime,
services,
selectedTime,
selectedTags,
]);
if (loading) {
return <Spinner tip="Loading..." />;
}
return (
<Space direction="vertical" style={{ width: '100%' }}>
<ReleaseNote path={location.pathname} />
<ResourceAttributesFilter />
<MetricTable />
</Space>
);
}
interface DispatchProps {
getService: (
props: GetServiceProps,
) => (dispatch: Dispatch<AppActions>, getState: () => AppState) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getService: bindActionCreators(GetService, dispatch),
});
type MetricsProps = DispatchProps;
export default connect(null, mapDispatchToProps)(Metrics);

View File

@ -0,0 +1,70 @@
import { Space } from 'antd';
import localStorageGet from 'api/browser/localstorage/get';
import localStorageSet from 'api/browser/localstorage/set';
import ReleaseNote from 'components/ReleaseNote';
import { SKIP_ONBOARDING } from 'constants/onboarding';
import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
import ServicesTable from 'container/ServiceTable';
import SkipOnBoardingModal from 'container/ServiceTable/SkipOnBoardModal';
import useErrorNotification from 'hooks/useErrorNotification';
import { useQueryService } from 'hooks/useQueryService';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { convertRawQueriesToTraceSelectedTags } from 'hooks/useResourceAttribute/utils';
import { useMemo, useState } from 'react';
import { useSelector } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
function Metrics(): JSX.Element {
const { minTime, maxTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const location = useLocation();
const { queries } = useResourceAttribute();
const [skipOnboarding, setSkipOnboarding] = useState(
localStorageGet(SKIP_ONBOARDING) === 'true',
);
const onContinueClick = (): void => {
localStorageSet(SKIP_ONBOARDING, 'true');
setSkipOnboarding(true);
};
const selectedTags = useMemo(
() => (convertRawQueriesToTraceSelectedTags(queries, '') as Tags[]) || [],
[queries],
);
const { data, error, isLoading, isError } = useQueryService({
minTime,
maxTime,
selectedTime,
selectedTags,
});
useErrorNotification(error);
if (
data?.length === 0 &&
isLoading === false &&
!skipOnboarding &&
isError === true
) {
return <SkipOnBoardingModal onContinueClick={onContinueClick} />;
}
return (
<Space direction="vertical" style={{ width: '100%' }}>
<ReleaseNote path={location.pathname} />
<ResourceAttributesFilter />
<ServicesTable services={data || []} isLoading={isLoading} />
</Space>
);
}
export default Metrics;

View File

@ -10,7 +10,6 @@ import {
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
import ROUTES from 'constants/routes';
import ExportPanel from 'container/ExportPanel';
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
import { SIGNOZ_VALUE } from 'container/QueryBuilder/filters/OrderByFilter/constants';
import QuerySection from 'container/TracesExplorer/QuerySection';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
@ -149,7 +148,7 @@ function TracesExplorer(): JSX.Element {
);
const getUpdateQuery = useCallback(
(newPanelType: GRAPH_TYPES): Query => {
(newPanelType: PANEL_TYPES): Query => {
let query = updateAllQueriesOperators(
currentQuery,
newPanelType,
@ -174,7 +173,7 @@ function TracesExplorer(): JSX.Element {
const handleTabChange = useCallback(
(type: string): void => {
const newPanelType = type as GRAPH_TYPES;
const newPanelType = type as PANEL_TYPES;
if (panelType === newPanelType) return;
const query = getUpdateQuery(newPanelType);

Some files were not shown because too many files have changed in this diff Show More