mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-13 08:08:59 +08:00
feat: implement metrics explorer summary section (#7200)
This commit is contained in:
parent
52693eb53e
commit
c2d038c025
@ -47,6 +47,7 @@
|
|||||||
"@tanstack/react-virtual": "3.11.2",
|
"@tanstack/react-virtual": "3.11.2",
|
||||||
"@uiw/react-md-editor": "3.23.5",
|
"@uiw/react-md-editor": "3.23.5",
|
||||||
"@visx/group": "3.3.0",
|
"@visx/group": "3.3.0",
|
||||||
|
"@visx/hierarchy": "3.12.0",
|
||||||
"@visx/shape": "3.5.0",
|
"@visx/shape": "3.5.0",
|
||||||
"@visx/tooltip": "3.3.0",
|
"@visx/tooltip": "3.3.0",
|
||||||
"@xstate/react": "^3.0.0",
|
"@xstate/react": "^3.0.0",
|
||||||
@ -69,6 +70,7 @@
|
|||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"css-loader": "5.0.0",
|
"css-loader": "5.0.0",
|
||||||
"css-minimizer-webpack-plugin": "5.0.1",
|
"css-minimizer-webpack-plugin": "5.0.1",
|
||||||
|
"d3-hierarchy": "3.1.2",
|
||||||
"dayjs": "^1.10.7",
|
"dayjs": "^1.10.7",
|
||||||
"dompurify": "3.1.3",
|
"dompurify": "3.1.3",
|
||||||
"dotenv": "8.2.0",
|
"dotenv": "8.2.0",
|
||||||
|
67
frontend/src/api/metricsExplorer/getMetricsList.ts
Normal file
67
frontend/src/api/metricsExplorer/getMetricsList.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import {
|
||||||
|
OrderByPayload,
|
||||||
|
TreemapViewType,
|
||||||
|
} from 'container/MetricsExplorer/Summary/types';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export interface MetricsListPayload {
|
||||||
|
filters: TagFilter;
|
||||||
|
groupBy?: BaseAutocompleteData[];
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
orderBy?: OrderByPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MetricType {
|
||||||
|
SUM = 'Sum',
|
||||||
|
GAUGE = 'Gauge',
|
||||||
|
HISTOGRAM = 'Histogram',
|
||||||
|
SUMMARY = 'Summary',
|
||||||
|
EXPONENTIAL_HISTOGRAM = 'ExponentialHistogram',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricsListItemData {
|
||||||
|
metric_name: string;
|
||||||
|
description: string;
|
||||||
|
type: MetricType;
|
||||||
|
unit: string;
|
||||||
|
[TreemapViewType.CARDINALITY]: number;
|
||||||
|
[TreemapViewType.DATAPOINTS]: number;
|
||||||
|
lastReceived: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricsListResponse {
|
||||||
|
status: string;
|
||||||
|
data: {
|
||||||
|
metrics: MetricsListItemData[];
|
||||||
|
total?: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMetricsList = async (
|
||||||
|
props: MetricsListPayload,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
): Promise<SuccessResponse<MetricsListResponse> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/metrics', props, {
|
||||||
|
signal,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
params: props,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
34
frontend/src/api/metricsExplorer/getMetricsListFilterKeys.ts
Normal file
34
frontend/src/api/metricsExplorer/getMetricsListFilterKeys.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
|
||||||
|
export interface MetricsListFilterKeysResponse {
|
||||||
|
status: string;
|
||||||
|
data: {
|
||||||
|
metricColumns: string[];
|
||||||
|
attributeKeys: BaseAutocompleteData[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMetricsListFilterKeys = async (
|
||||||
|
signal?: AbortSignal,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
): Promise<SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get('/metrics/filters/keys', {
|
||||||
|
signal,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,44 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
|
||||||
|
export interface MetricsListFilterValuesPayload {
|
||||||
|
filterAttributeKeyDataType: string;
|
||||||
|
filterKey: string;
|
||||||
|
searchText: string;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricsListFilterValuesResponse {
|
||||||
|
status: string;
|
||||||
|
data: {
|
||||||
|
FilterValues: BaseAutocompleteData[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMetricsListFilterValues = async (
|
||||||
|
props: MetricsListFilterValuesPayload,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
): Promise<
|
||||||
|
SuccessResponse<MetricsListFilterValuesResponse> | ErrorResponse
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/metrics/filters/values', props, {
|
||||||
|
signal,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
params: props,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
54
frontend/src/api/metricsExplorer/getMetricsTreeMap.ts
Normal file
54
frontend/src/api/metricsExplorer/getMetricsTreeMap.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import axios from 'api';
|
||||||
|
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { TreemapViewType } from 'container/MetricsExplorer/Summary/types';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export interface MetricsTreeMapPayload {
|
||||||
|
filters: TagFilter;
|
||||||
|
limit?: number;
|
||||||
|
treemap?: TreemapViewType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricsTreeMapResponse {
|
||||||
|
status: string;
|
||||||
|
data: {
|
||||||
|
[TreemapViewType.CARDINALITY]: CardinalityData[];
|
||||||
|
[TreemapViewType.DATAPOINTS]: DatapointsData[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardinalityData {
|
||||||
|
percentage: number;
|
||||||
|
total_value: number;
|
||||||
|
metric_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DatapointsData {
|
||||||
|
percentage: number;
|
||||||
|
metric_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMetricsTreeMap = async (
|
||||||
|
props: MetricsTreeMapPayload,
|
||||||
|
signal?: AbortSignal,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
): Promise<SuccessResponse<MetricsTreeMapResponse> | ErrorResponse> => {
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/metrics/treemap', props, {
|
||||||
|
signal,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode: 200,
|
||||||
|
error: null,
|
||||||
|
message: response.data.status,
|
||||||
|
payload: response.data,
|
||||||
|
params: props,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return ErrorResponseHandler(error as AxiosError);
|
||||||
|
}
|
||||||
|
};
|
@ -43,4 +43,10 @@ export const REACT_QUERY_KEY = {
|
|||||||
AWS_GENERATE_CONNECTION_URL: 'AWS_GENERATE_CONNECTION_URL',
|
AWS_GENERATE_CONNECTION_URL: 'AWS_GENERATE_CONNECTION_URL',
|
||||||
AWS_GET_CONNECTION_PARAMS: 'AWS_GET_CONNECTION_PARAMS',
|
AWS_GET_CONNECTION_PARAMS: 'AWS_GET_CONNECTION_PARAMS',
|
||||||
GET_ATTRIBUTE_VALUES: 'GET_ATTRIBUTE_VALUES',
|
GET_ATTRIBUTE_VALUES: 'GET_ATTRIBUTE_VALUES',
|
||||||
|
|
||||||
|
// Metrics Explorer Query Keys
|
||||||
|
GET_METRICS_LIST: 'GET_METRICS_LIST',
|
||||||
|
GET_METRICS_TREE_MAP: 'GET_METRICS_TREE_MAP',
|
||||||
|
GET_METRICS_LIST_FILTER_KEYS: 'GET_METRICS_LIST_FILTER_KEYS',
|
||||||
|
GET_METRICS_LIST_FILTER_VALUES: 'GET_METRICS_LIST_FILTER_VALUES',
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
import { Select } from 'antd';
|
||||||
|
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
|
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||||
|
import { HardHat } from 'lucide-react';
|
||||||
|
|
||||||
|
import { TREEMAP_VIEW_OPTIONS } from './constants';
|
||||||
|
import { MetricsSearchProps } from './types';
|
||||||
|
|
||||||
|
function MetricsSearch({
|
||||||
|
query,
|
||||||
|
onChange,
|
||||||
|
heatmapView,
|
||||||
|
setHeatmapView,
|
||||||
|
}: MetricsSearchProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="metrics-search-container">
|
||||||
|
<div className="metrics-search-options">
|
||||||
|
<Select
|
||||||
|
style={{ width: 140 }}
|
||||||
|
options={TREEMAP_VIEW_OPTIONS}
|
||||||
|
value={heatmapView}
|
||||||
|
onChange={setHeatmapView}
|
||||||
|
/>
|
||||||
|
<DateTimeSelectionV2
|
||||||
|
showAutoRefresh={false}
|
||||||
|
showRefreshText={false}
|
||||||
|
hideShareModal
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<QueryBuilderSearch
|
||||||
|
query={query}
|
||||||
|
onChange={onChange}
|
||||||
|
suffixIcon={<HardHat size={16} />}
|
||||||
|
isMetricsExplorer
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MetricsSearch;
|
@ -0,0 +1,86 @@
|
|||||||
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
Spin,
|
||||||
|
Table,
|
||||||
|
TablePaginationConfig,
|
||||||
|
TableProps,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
|
import { SorterResult } from 'antd/es/table/interface';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { MetricsListItemRowData, MetricsTableProps } from './types';
|
||||||
|
import { metricsTableColumns } from './utils';
|
||||||
|
|
||||||
|
function MetricsTable({
|
||||||
|
isLoading,
|
||||||
|
data,
|
||||||
|
pageSize,
|
||||||
|
currentPage,
|
||||||
|
onPaginationChange,
|
||||||
|
setOrderBy,
|
||||||
|
totalCount,
|
||||||
|
}: MetricsTableProps): JSX.Element {
|
||||||
|
const handleTableChange: TableProps<MetricsListItemRowData>['onChange'] = useCallback(
|
||||||
|
(
|
||||||
|
_pagination: TablePaginationConfig,
|
||||||
|
_filters: Record<string, (string | number | boolean)[] | null>,
|
||||||
|
sorter:
|
||||||
|
| SorterResult<MetricsListItemRowData>
|
||||||
|
| SorterResult<MetricsListItemRowData>[],
|
||||||
|
): void => {
|
||||||
|
if ('field' in sorter && sorter.order) {
|
||||||
|
setOrderBy({
|
||||||
|
columnName: sorter.field as string,
|
||||||
|
order: sorter.order === 'ascend' ? 'asc' : 'desc',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setOrderBy({
|
||||||
|
columnName: 'type',
|
||||||
|
order: 'asc',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setOrderBy],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="metrics-table-container">
|
||||||
|
<Table
|
||||||
|
loading={{
|
||||||
|
spinning: isLoading,
|
||||||
|
indicator: <Spin indicator={<LoadingOutlined size={14} spin />} />,
|
||||||
|
}}
|
||||||
|
dataSource={data}
|
||||||
|
columns={metricsTableColumns}
|
||||||
|
locale={{
|
||||||
|
emptyText: (
|
||||||
|
<div className="no-metrics-message-container">
|
||||||
|
<img
|
||||||
|
src="/Icons/emptyState.svg"
|
||||||
|
alt="thinking-emoji"
|
||||||
|
className="empty-state-svg"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Typography.Text className="no-metrics-message">
|
||||||
|
This query had no results. Edit your query and try again!
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
tableLayout="fixed"
|
||||||
|
onChange={handleTableChange}
|
||||||
|
pagination={{
|
||||||
|
current: currentPage,
|
||||||
|
pageSize,
|
||||||
|
showSizeChanger: true,
|
||||||
|
hideOnSinglePage: false,
|
||||||
|
onChange: onPaginationChange,
|
||||||
|
total: totalCount,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MetricsTable;
|
@ -0,0 +1,130 @@
|
|||||||
|
import { Group } from '@visx/group';
|
||||||
|
import { Treemap } from '@visx/hierarchy';
|
||||||
|
import { Empty, Skeleton, Tooltip } from 'antd';
|
||||||
|
import { stratify, treemapBinary } from 'd3-hierarchy';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useWindowSize } from 'react-use';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TREEMAP_HEIGHT,
|
||||||
|
TREEMAP_MARGINS,
|
||||||
|
TREEMAP_SQUARE_PADDING,
|
||||||
|
} from './constants';
|
||||||
|
import { TreemapProps, TreemapTile, TreemapViewType } from './types';
|
||||||
|
import {
|
||||||
|
getTreemapTileStyle,
|
||||||
|
getTreemapTileTextStyle,
|
||||||
|
transformTreemapData,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
function MetricsTreemap({
|
||||||
|
viewType,
|
||||||
|
data,
|
||||||
|
isLoading,
|
||||||
|
}: TreemapProps): JSX.Element {
|
||||||
|
const { width: windowWidth } = useWindowSize();
|
||||||
|
|
||||||
|
const treemapWidth = useMemo(
|
||||||
|
() =>
|
||||||
|
Math.max(
|
||||||
|
windowWidth - TREEMAP_MARGINS.LEFT - TREEMAP_MARGINS.RIGHT - 70,
|
||||||
|
300,
|
||||||
|
),
|
||||||
|
[windowWidth],
|
||||||
|
);
|
||||||
|
|
||||||
|
const treemapData = useMemo(() => {
|
||||||
|
const extracedTreemapData =
|
||||||
|
(viewType === TreemapViewType.CARDINALITY
|
||||||
|
? data?.data?.[TreemapViewType.CARDINALITY]
|
||||||
|
: data?.data?.[TreemapViewType.DATAPOINTS]) || [];
|
||||||
|
return transformTreemapData(extracedTreemapData, viewType);
|
||||||
|
}, [data, viewType]);
|
||||||
|
|
||||||
|
const transformedTreemapData = stratify<TreemapTile>()
|
||||||
|
.id((d) => d.id)
|
||||||
|
.parentId((d) => d.parent)(treemapData)
|
||||||
|
.sum((d) => d.size ?? 0);
|
||||||
|
|
||||||
|
const xMax = treemapWidth - TREEMAP_MARGINS.LEFT - TREEMAP_MARGINS.RIGHT;
|
||||||
|
const yMax = TREEMAP_HEIGHT - TREEMAP_MARGINS.TOP - TREEMAP_MARGINS.BOTTOM;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Skeleton style={{ width: treemapWidth, height: TREEMAP_HEIGHT }} active />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!data ||
|
||||||
|
!data.data ||
|
||||||
|
data?.status === 'error' ||
|
||||||
|
(data?.status === 'success' && !data?.data?.[viewType])
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Empty
|
||||||
|
description="No metrics found"
|
||||||
|
style={{ width: treemapWidth, height: TREEMAP_HEIGHT, paddingTop: 30 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="metrics-treemap">
|
||||||
|
<svg width={treemapWidth} height={TREEMAP_HEIGHT}>
|
||||||
|
<rect
|
||||||
|
width={treemapWidth}
|
||||||
|
height={TREEMAP_HEIGHT}
|
||||||
|
rx={14}
|
||||||
|
fill="transparent"
|
||||||
|
/>
|
||||||
|
<Treemap<TreemapTile>
|
||||||
|
top={TREEMAP_MARGINS.TOP}
|
||||||
|
root={transformedTreemapData}
|
||||||
|
size={[xMax, yMax]}
|
||||||
|
tile={treemapBinary}
|
||||||
|
round
|
||||||
|
>
|
||||||
|
{(treemap): JSX.Element => (
|
||||||
|
<Group>
|
||||||
|
{treemap
|
||||||
|
.descendants()
|
||||||
|
.reverse()
|
||||||
|
.map((node, i) => {
|
||||||
|
const nodeWidth = node.x1 - node.x0 - TREEMAP_SQUARE_PADDING;
|
||||||
|
const nodeHeight = node.y1 - node.y0 - TREEMAP_SQUARE_PADDING;
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
// eslint-disable-next-line react/no-array-index-key
|
||||||
|
key={node.data.id || `node-${i}`}
|
||||||
|
top={node.y0 + TREEMAP_MARGINS.TOP}
|
||||||
|
left={node.x0 + TREEMAP_MARGINS.LEFT}
|
||||||
|
>
|
||||||
|
{node.depth > 0 && (
|
||||||
|
<Tooltip
|
||||||
|
title={`${node.data.id}: ${node.data.displayValue}%`}
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<foreignObject
|
||||||
|
width={nodeWidth}
|
||||||
|
height={nodeHeight}
|
||||||
|
style={getTreemapTileStyle(node.data)}
|
||||||
|
>
|
||||||
|
<div style={getTreemapTileTextStyle()}>
|
||||||
|
{`${node.data.displayValue}%`}
|
||||||
|
</div>
|
||||||
|
</foreignObject>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Treemap>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MetricsTreemap;
|
@ -0,0 +1,223 @@
|
|||||||
|
.metrics-explorer-summary-tab {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 0;
|
||||||
|
|
||||||
|
.metrics-search-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
.metrics-search-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-table-container {
|
||||||
|
margin-left: -16px;
|
||||||
|
margin-right: -16px;
|
||||||
|
|
||||||
|
.ant-table {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
padding: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
color: var(--Vanilla-400, #c0c1c3);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px; /* 163.636% */
|
||||||
|
letter-spacing: 0.44px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-thead > tr > th:has(.metric-name-column-header) {
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:has(.metric-name-column-value) {
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-name-column-value {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: 'Geist Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-cell {
|
||||||
|
.active-tag {
|
||||||
|
color: var(--bg-forest-500);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-container {
|
||||||
|
.ant-progress-bg {
|
||||||
|
height: 8px !important;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:first-child {
|
||||||
|
text-align: justify;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:nth-child(2) {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:nth-child(n + 3) {
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
.column-header-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.column-header-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-thead
|
||||||
|
> tr
|
||||||
|
> th:not(:last-child):not(.ant-table-selection-column):not(.ant-table-row-expand-icon-cell):not([colspan])::before {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-empty-normal {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pagination {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: calc(100% - 64px);
|
||||||
|
background: var(--bg-ink-500);
|
||||||
|
padding: 16px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
// this is to offset intercom icon till we improve the design
|
||||||
|
right: 20px;
|
||||||
|
|
||||||
|
.ant-pagination-item {
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&-active {
|
||||||
|
background: var(--bg-robin-500);
|
||||||
|
border-color: var(--bg-robin-500);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--bg-ink-500) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-metrics-message-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 400px;
|
||||||
|
gap: 16px;
|
||||||
|
padding-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-type-renderer {
|
||||||
|
border-radius: 50px;
|
||||||
|
height: 24px;
|
||||||
|
width: fit-content;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
align-self: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.metrics-table-container {
|
||||||
|
.ant-table {
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-thead > tr > th:has(.metric-name-column-header) {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--bg-ink-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-cell:has(.metric-name-column-value) {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-name-column-value {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-pagination {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
|
||||||
|
.ant-pagination-item {
|
||||||
|
&-active {
|
||||||
|
background: var(--bg-robin-500);
|
||||||
|
border-color: var(--bg-robin-500);
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--bg-vanilla-100) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,162 @@
|
|||||||
|
import './Summary.styles.scss';
|
||||||
|
|
||||||
import * as Sentry from '@sentry/react';
|
import * as Sentry from '@sentry/react';
|
||||||
|
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
|
||||||
|
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
|
||||||
|
import { useGetMetricsTreeMap } from 'hooks/metricsExplorer/useGetMetricsTreeMap';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
import MetricsSearch from './MetricsSearch';
|
||||||
|
import MetricsTable from './MetricsTable';
|
||||||
|
import MetricsTreemap from './MetricsTreemap';
|
||||||
|
import { OrderByPayload, TreemapViewType } from './types';
|
||||||
|
import {
|
||||||
|
convertNanoToMilliseconds,
|
||||||
|
formatDataForMetricsTable,
|
||||||
|
getMetricsListQuery,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
function Summary(): JSX.Element {
|
function Summary(): JSX.Element {
|
||||||
|
const { pageSize, setPageSize } = usePageSize('metricsExplorer');
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [orderBy, setOrderBy] = useState<OrderByPayload>({
|
||||||
|
columnName: 'type',
|
||||||
|
order: 'asc',
|
||||||
|
});
|
||||||
|
const [heatmapView, setHeatmapView] = useState<TreemapViewType>(
|
||||||
|
TreemapViewType.CARDINALITY,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { currentQuery } = useQueryBuilder();
|
||||||
|
const queryFilters = useMemo(
|
||||||
|
() =>
|
||||||
|
currentQuery?.builder?.queryData[0]?.filters || {
|
||||||
|
items: [],
|
||||||
|
op: 'and',
|
||||||
|
},
|
||||||
|
[currentQuery],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { handleChangeQueryData } = useQueryOperations({
|
||||||
|
index: 0,
|
||||||
|
query: currentQuery.builder.queryData[0],
|
||||||
|
entityVersion: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const metricsListQuery = useMemo(() => {
|
||||||
|
const baseQuery = getMetricsListQuery();
|
||||||
|
return {
|
||||||
|
...baseQuery,
|
||||||
|
limit: pageSize,
|
||||||
|
offset: (currentPage - 1) * pageSize,
|
||||||
|
filters: queryFilters,
|
||||||
|
start: convertNanoToMilliseconds(minTime),
|
||||||
|
end: convertNanoToMilliseconds(maxTime),
|
||||||
|
orderBy,
|
||||||
|
};
|
||||||
|
}, [queryFilters, minTime, maxTime, orderBy, pageSize, currentPage]);
|
||||||
|
|
||||||
|
const metricsTreemapQuery = useMemo(
|
||||||
|
() => ({
|
||||||
|
limit: 100,
|
||||||
|
filters: queryFilters,
|
||||||
|
treemap: heatmapView,
|
||||||
|
start: convertNanoToMilliseconds(minTime),
|
||||||
|
end: convertNanoToMilliseconds(maxTime),
|
||||||
|
}),
|
||||||
|
[queryFilters, heatmapView, minTime, maxTime],
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: metricsData,
|
||||||
|
isLoading: isMetricsLoading,
|
||||||
|
isFetching: isMetricsFetching,
|
||||||
|
} = useGetMetricsList(metricsListQuery, {
|
||||||
|
enabled: !!metricsListQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: treeMapData,
|
||||||
|
isLoading: isTreeMapLoading,
|
||||||
|
isFetching: isTreeMapFetching,
|
||||||
|
} = useGetMetricsTreeMap(metricsTreemapQuery, {
|
||||||
|
enabled: !!metricsTreemapQuery,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFilterChange = useCallback(
|
||||||
|
(value: TagFilter) => {
|
||||||
|
handleChangeQueryData('filters', value);
|
||||||
|
setCurrentPage(1);
|
||||||
|
},
|
||||||
|
[handleChangeQueryData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedCurrentQuery = useMemo(
|
||||||
|
() => ({
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: [
|
||||||
|
{
|
||||||
|
...currentQuery.builder.queryData[0],
|
||||||
|
aggregateOperator: 'noop',
|
||||||
|
aggregateAttribute: {
|
||||||
|
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[currentQuery],
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchQuery = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||||
|
|
||||||
|
const onPaginationChange = (page: number, pageSize: number): void => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
setPageSize(pageSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
const formattedMetricsData = useMemo(
|
||||||
|
() => formatDataForMetricsTable(metricsData?.payload?.data?.metrics || []),
|
||||||
|
[metricsData],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
<Sentry.ErrorBoundary fallback={<ErrorBoundaryFallback />}>
|
||||||
Summary
|
<div className="metrics-explorer-summary-tab">
|
||||||
|
<MetricsSearch
|
||||||
|
query={searchQuery}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
heatmapView={heatmapView}
|
||||||
|
setHeatmapView={setHeatmapView}
|
||||||
|
/>
|
||||||
|
<MetricsTreemap
|
||||||
|
data={treeMapData?.payload}
|
||||||
|
isLoading={isTreeMapLoading || isTreeMapFetching}
|
||||||
|
viewType={heatmapView}
|
||||||
|
/>
|
||||||
|
<MetricsTable
|
||||||
|
isLoading={isMetricsLoading || isMetricsFetching}
|
||||||
|
data={formattedMetricsData}
|
||||||
|
pageSize={pageSize}
|
||||||
|
currentPage={currentPage}
|
||||||
|
onPaginationChange={onPaginationChange}
|
||||||
|
setOrderBy={setOrderBy}
|
||||||
|
totalCount={metricsData?.payload?.data.total || 0}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Sentry.ErrorBoundary>
|
</Sentry.ErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
26
frontend/src/container/MetricsExplorer/Summary/constants.ts
Normal file
26
frontend/src/container/MetricsExplorer/Summary/constants.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||||
|
|
||||||
|
import { TreemapViewType } from './types';
|
||||||
|
|
||||||
|
export const METRICS_TABLE_PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
export const TREEMAP_VIEW_OPTIONS: {
|
||||||
|
value: TreemapViewType;
|
||||||
|
label: string;
|
||||||
|
}[] = [
|
||||||
|
{ value: TreemapViewType.CARDINALITY, label: 'Cardinality' },
|
||||||
|
{ value: TreemapViewType.DATAPOINTS, label: 'Datapoints' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TREEMAP_HEIGHT = 300;
|
||||||
|
export const TREEMAP_SQUARE_PADDING = 5;
|
||||||
|
|
||||||
|
export const TREEMAP_MARGINS = { TOP: 10, LEFT: 10, RIGHT: 10, BOTTOM: 10 };
|
||||||
|
|
||||||
|
export const METRIC_TYPE_LABEL_MAP = {
|
||||||
|
[MetricType.SUM]: 'Sum',
|
||||||
|
[MetricType.GAUGE]: 'Gauge',
|
||||||
|
[MetricType.HISTOGRAM]: 'Histogram',
|
||||||
|
[MetricType.SUMMARY]: 'Summary',
|
||||||
|
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
|
||||||
|
};
|
56
frontend/src/container/MetricsExplorer/Summary/types.ts
Normal file
56
frontend/src/container/MetricsExplorer/Summary/types.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { MetricsTreeMapResponse } from 'api/metricsExplorer/getMetricsTreeMap';
|
||||||
|
import React, { Dispatch, SetStateAction } from 'react';
|
||||||
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
TagFilter,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
export interface MetricsTableProps {
|
||||||
|
isLoading: boolean;
|
||||||
|
data: MetricsListItemRowData[];
|
||||||
|
pageSize: number;
|
||||||
|
currentPage: number;
|
||||||
|
onPaginationChange: (page: number, pageSize: number) => void;
|
||||||
|
setOrderBy: Dispatch<SetStateAction<OrderByPayload>>;
|
||||||
|
totalCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricsSearchProps {
|
||||||
|
query: IBuilderQuery;
|
||||||
|
onChange: (value: TagFilter) => void;
|
||||||
|
heatmapView: TreemapViewType;
|
||||||
|
setHeatmapView: (value: TreemapViewType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreemapProps {
|
||||||
|
data: MetricsTreeMapResponse | null | undefined;
|
||||||
|
isLoading: boolean;
|
||||||
|
viewType: TreemapViewType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderByPayload {
|
||||||
|
columnName: string;
|
||||||
|
order: 'asc' | 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetricsListItemRowData {
|
||||||
|
key: string;
|
||||||
|
metric_name: React.ReactNode;
|
||||||
|
description: React.ReactNode;
|
||||||
|
metric_type: React.ReactNode;
|
||||||
|
unit: React.ReactNode;
|
||||||
|
samples: React.ReactNode;
|
||||||
|
timeseries: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum TreemapViewType {
|
||||||
|
CARDINALITY = 'timeseries',
|
||||||
|
DATAPOINTS = 'samples',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreemapTile {
|
||||||
|
id: string;
|
||||||
|
size: number;
|
||||||
|
displayValue: number | string | null;
|
||||||
|
parent: string | null;
|
||||||
|
}
|
241
frontend/src/container/MetricsExplorer/Summary/utils.tsx
Normal file
241
frontend/src/container/MetricsExplorer/Summary/utils.tsx
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { Tooltip, Typography } from 'antd';
|
||||||
|
import { ColumnType } from 'antd/es/table';
|
||||||
|
import {
|
||||||
|
MetricsListItemData,
|
||||||
|
MetricsListPayload,
|
||||||
|
MetricType,
|
||||||
|
} from 'api/metricsExplorer/getMetricsList';
|
||||||
|
import {
|
||||||
|
CardinalityData,
|
||||||
|
DatapointsData,
|
||||||
|
} from 'api/metricsExplorer/getMetricsTreeMap';
|
||||||
|
import {
|
||||||
|
BarChart,
|
||||||
|
BarChart2,
|
||||||
|
BarChartHorizontal,
|
||||||
|
Diff,
|
||||||
|
Gauge,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { METRIC_TYPE_LABEL_MAP } from './constants';
|
||||||
|
import { MetricsListItemRowData, TreemapTile, TreemapViewType } from './types';
|
||||||
|
|
||||||
|
export const metricsTableColumns: ColumnType<MetricsListItemRowData>[] = [
|
||||||
|
{
|
||||||
|
title: <div className="metric-name-column-header">METRIC</div>,
|
||||||
|
dataIndex: 'metric_name',
|
||||||
|
width: 400,
|
||||||
|
sorter: true,
|
||||||
|
className: 'metric-name-column-header',
|
||||||
|
render: (value: string): React.ReactNode => (
|
||||||
|
<div className="metric-name-column-value">{value}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'DESCRIPTION',
|
||||||
|
dataIndex: 'description',
|
||||||
|
width: 400,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'TYPE',
|
||||||
|
dataIndex: 'metric_type',
|
||||||
|
sorter: true,
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'UNIT',
|
||||||
|
dataIndex: 'unit',
|
||||||
|
width: 150,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'DATAPOINTS',
|
||||||
|
dataIndex: TreemapViewType.DATAPOINTS,
|
||||||
|
width: 150,
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'CARDINALITY',
|
||||||
|
dataIndex: TreemapViewType.CARDINALITY,
|
||||||
|
width: 150,
|
||||||
|
sorter: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const getMetricsListQuery = (): MetricsListPayload => ({
|
||||||
|
filters: {
|
||||||
|
items: [],
|
||||||
|
op: 'and',
|
||||||
|
},
|
||||||
|
orderBy: { columnName: 'metric_name', order: 'asc' },
|
||||||
|
});
|
||||||
|
|
||||||
|
function MetricTypeRenderer({ type }: { type: MetricType }): JSX.Element {
|
||||||
|
const [icon, color] = useMemo(() => {
|
||||||
|
switch (type) {
|
||||||
|
case MetricType.SUM:
|
||||||
|
return [
|
||||||
|
<Diff key={type} size={12} color={Color.BG_ROBIN_500} />,
|
||||||
|
Color.BG_ROBIN_500,
|
||||||
|
];
|
||||||
|
case MetricType.GAUGE:
|
||||||
|
return [
|
||||||
|
<Gauge key={type} size={12} color={Color.BG_SAKURA_500} />,
|
||||||
|
Color.BG_SAKURA_500,
|
||||||
|
];
|
||||||
|
case MetricType.HISTOGRAM:
|
||||||
|
return [
|
||||||
|
<BarChart2 key={type} size={12} color={Color.BG_SIENNA_500} />,
|
||||||
|
Color.BG_SIENNA_500,
|
||||||
|
];
|
||||||
|
case MetricType.SUMMARY:
|
||||||
|
return [
|
||||||
|
<BarChartHorizontal key={type} size={12} color={Color.BG_FOREST_500} />,
|
||||||
|
Color.BG_FOREST_500,
|
||||||
|
];
|
||||||
|
case MetricType.EXPONENTIAL_HISTOGRAM:
|
||||||
|
return [
|
||||||
|
<BarChart key={type} size={12} color={Color.BG_AQUA_500} />,
|
||||||
|
Color.BG_AQUA_500,
|
||||||
|
];
|
||||||
|
default:
|
||||||
|
return [null, ''];
|
||||||
|
}
|
||||||
|
}, [type]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="metric-type-renderer"
|
||||||
|
style={{
|
||||||
|
backgroundColor: `${color}33`,
|
||||||
|
border: `1px solid ${color}`,
|
||||||
|
color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
<Typography.Text style={{ color, fontSize: 12 }}>
|
||||||
|
{METRIC_TYPE_LABEL_MAP[type]}
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ValidateRowValueWrapper({
|
||||||
|
value,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
value: string | number | null;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}): JSX.Element {
|
||||||
|
if (!value) {
|
||||||
|
return <div>-</div>;
|
||||||
|
}
|
||||||
|
return <div>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatDataForMetricsTable = (
|
||||||
|
data: MetricsListItemData[],
|
||||||
|
): MetricsListItemRowData[] =>
|
||||||
|
data.map((metric) => ({
|
||||||
|
key: metric.metric_name,
|
||||||
|
metric_name: (
|
||||||
|
<ValidateRowValueWrapper value={metric.metric_name}>
|
||||||
|
<Tooltip title={metric.metric_name}>{metric.metric_name}</Tooltip>
|
||||||
|
</ValidateRowValueWrapper>
|
||||||
|
),
|
||||||
|
description: (
|
||||||
|
<ValidateRowValueWrapper value={metric.description}>
|
||||||
|
<Tooltip title={metric.description}>{metric.description}</Tooltip>
|
||||||
|
</ValidateRowValueWrapper>
|
||||||
|
),
|
||||||
|
metric_type: <MetricTypeRenderer type={metric.type} />,
|
||||||
|
unit: (
|
||||||
|
<ValidateRowValueWrapper value={metric.unit}>
|
||||||
|
{metric.unit}
|
||||||
|
</ValidateRowValueWrapper>
|
||||||
|
),
|
||||||
|
[TreemapViewType.DATAPOINTS]: (
|
||||||
|
<ValidateRowValueWrapper value={metric[TreemapViewType.DATAPOINTS]}>
|
||||||
|
{metric[TreemapViewType.DATAPOINTS]}
|
||||||
|
</ValidateRowValueWrapper>
|
||||||
|
),
|
||||||
|
[TreemapViewType.CARDINALITY]: (
|
||||||
|
<ValidateRowValueWrapper value={metric[TreemapViewType.CARDINALITY]}>
|
||||||
|
{metric[TreemapViewType.CARDINALITY]}
|
||||||
|
</ValidateRowValueWrapper>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const transformTreemapData = (
|
||||||
|
data: CardinalityData[] | DatapointsData[],
|
||||||
|
viewType: TreemapViewType,
|
||||||
|
): TreemapTile[] => {
|
||||||
|
const totalSize = (data as (CardinalityData | DatapointsData)[]).reduce(
|
||||||
|
(acc: number, item: CardinalityData | DatapointsData) =>
|
||||||
|
acc + item.percentage,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
const children = data.map((item) => ({
|
||||||
|
id: item.metric_name,
|
||||||
|
size: totalSize > 0 ? Number((item.percentage / totalSize).toFixed(2)) : 0,
|
||||||
|
displayValue: Number(item.percentage).toFixed(2),
|
||||||
|
parent: viewType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: viewType,
|
||||||
|
size: 0,
|
||||||
|
parent: null,
|
||||||
|
displayValue: null,
|
||||||
|
},
|
||||||
|
...children,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTreemapTileBackgroundColor = (node: TreemapTile): string => {
|
||||||
|
const size = node.size * 10;
|
||||||
|
if (size > 0.8) {
|
||||||
|
return Color.BG_AMBER_600;
|
||||||
|
}
|
||||||
|
if (size > 0.6) {
|
||||||
|
return Color.BG_AMBER_500;
|
||||||
|
}
|
||||||
|
if (size > 0.4) {
|
||||||
|
return Color.BG_AMBER_400;
|
||||||
|
}
|
||||||
|
if (size > 0.2) {
|
||||||
|
return Color.BG_AMBER_300;
|
||||||
|
}
|
||||||
|
if (size > 0.1) {
|
||||||
|
return Color.BG_AMBER_200;
|
||||||
|
}
|
||||||
|
return Color.BG_AMBER_100;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getTreemapTileStyle = (
|
||||||
|
node: TreemapTile,
|
||||||
|
): React.CSSProperties => ({
|
||||||
|
overflow: 'visible',
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: getTreemapTileBackgroundColor(node),
|
||||||
|
borderRadius: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getTreemapTileTextStyle = (): React.CSSProperties => ({
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
fontSize: '12px',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: Color.TEXT_SLATE_400,
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '4px',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const convertNanoToMilliseconds = (time: number): number =>
|
||||||
|
Math.floor(time / 1000000);
|
@ -75,6 +75,7 @@ function QueryBuilderSearch({
|
|||||||
placeholder,
|
placeholder,
|
||||||
suffixIcon,
|
suffixIcon,
|
||||||
isInfraMonitoring,
|
isInfraMonitoring,
|
||||||
|
isMetricsExplorer,
|
||||||
disableNavigationShortcuts,
|
disableNavigationShortcuts,
|
||||||
entity,
|
entity,
|
||||||
}: QueryBuilderSearchProps): JSX.Element {
|
}: QueryBuilderSearchProps): JSX.Element {
|
||||||
@ -113,6 +114,7 @@ function QueryBuilderSearch({
|
|||||||
isLogsExplorerPage,
|
isLogsExplorerPage,
|
||||||
isInfraMonitoring,
|
isInfraMonitoring,
|
||||||
entity,
|
entity,
|
||||||
|
isMetricsExplorer,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||||
@ -129,6 +131,7 @@ function QueryBuilderSearch({
|
|||||||
isLogsExplorerPage,
|
isLogsExplorerPage,
|
||||||
isInfraMonitoring,
|
isInfraMonitoring,
|
||||||
entity,
|
entity,
|
||||||
|
isMetricsExplorer,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||||
@ -137,12 +140,12 @@ function QueryBuilderSearch({
|
|||||||
|
|
||||||
const toggleEditMode = useCallback(
|
const toggleEditMode = useCallback(
|
||||||
(value: boolean) => {
|
(value: boolean) => {
|
||||||
// Editing mode is required only in infra monitoring mode
|
// Editing mode is required only in infra monitoring or metrics explorer
|
||||||
if (isInfraMonitoring) {
|
if (isInfraMonitoring || isMetricsExplorer) {
|
||||||
setIsEditingTag(value);
|
setIsEditingTag(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isInfraMonitoring],
|
[isInfraMonitoring, isMetricsExplorer],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onTagRender = ({
|
const onTagRender = ({
|
||||||
@ -168,7 +171,7 @@ function QueryBuilderSearch({
|
|||||||
updateTag(value);
|
updateTag(value);
|
||||||
// Editing starts
|
// Editing starts
|
||||||
toggleEditMode(true);
|
toggleEditMode(true);
|
||||||
if (isInfraMonitoring) {
|
if (isInfraMonitoring || isMetricsExplorer) {
|
||||||
setSearchValue(value);
|
setSearchValue(value);
|
||||||
} else {
|
} else {
|
||||||
handleSearch(value);
|
handleSearch(value);
|
||||||
@ -240,8 +243,11 @@ function QueryBuilderSearch({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isMetricsDataSource = useMemo(
|
const isMetricsDataSource = useMemo(
|
||||||
() => query.dataSource === DataSource.METRICS && !isInfraMonitoring,
|
() =>
|
||||||
[query.dataSource, isInfraMonitoring],
|
query.dataSource === DataSource.METRICS &&
|
||||||
|
!isInfraMonitoring &&
|
||||||
|
!isMetricsExplorer,
|
||||||
|
[query.dataSource, isInfraMonitoring, isMetricsExplorer],
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchValueDataType = (value: unknown, operator: string): DataTypes => {
|
const fetchValueDataType = (value: unknown, operator: string): DataTypes => {
|
||||||
@ -291,8 +297,8 @@ function QueryBuilderSearch({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// If in infra monitoring, only run the onChange query when editing is finsished.
|
// If in infra monitoring or metrics explorer, only run the onChange query when editing is finsished.
|
||||||
if (isInfraMonitoring) {
|
if (isInfraMonitoring || isMetricsExplorer) {
|
||||||
if (!isEditingTag) {
|
if (!isEditingTag) {
|
||||||
onChange(initialTagFilters);
|
onChange(initialTagFilters);
|
||||||
}
|
}
|
||||||
@ -498,6 +504,7 @@ interface QueryBuilderSearchProps {
|
|||||||
isInfraMonitoring?: boolean;
|
isInfraMonitoring?: boolean;
|
||||||
disableNavigationShortcuts?: boolean;
|
disableNavigationShortcuts?: boolean;
|
||||||
entity?: K8sCategory | null;
|
entity?: K8sCategory | null;
|
||||||
|
isMetricsExplorer?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryBuilderSearch.defaultProps = {
|
QueryBuilderSearch.defaultProps = {
|
||||||
@ -508,6 +515,7 @@ QueryBuilderSearch.defaultProps = {
|
|||||||
isInfraMonitoring: false,
|
isInfraMonitoring: false,
|
||||||
disableNavigationShortcuts: false,
|
disableNavigationShortcuts: false,
|
||||||
entity: null,
|
entity: null,
|
||||||
|
isMetricsExplorer: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface CustomTagProps {
|
export interface CustomTagProps {
|
||||||
|
47
frontend/src/hooks/metricsExplorer/useGetMetricsList.ts
Normal file
47
frontend/src/hooks/metricsExplorer/useGetMetricsList.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import {
|
||||||
|
getMetricsList,
|
||||||
|
MetricsListPayload,
|
||||||
|
MetricsListResponse,
|
||||||
|
} from 'api/metricsExplorer/getMetricsList';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
|
type UseGetMetricsList = (
|
||||||
|
requestData: MetricsListPayload,
|
||||||
|
|
||||||
|
options?: UseQueryOptions<
|
||||||
|
SuccessResponse<MetricsListResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>,
|
||||||
|
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
) => UseQueryResult<
|
||||||
|
SuccessResponse<MetricsListResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useGetMetricsList: UseGetMetricsList = (
|
||||||
|
requestData,
|
||||||
|
options,
|
||||||
|
headers,
|
||||||
|
) => {
|
||||||
|
const queryKey = useMemo(() => {
|
||||||
|
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||||
|
return [...options.queryKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.queryKey && typeof options.queryKey === 'string') {
|
||||||
|
return options.queryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [REACT_QUERY_KEY.GET_METRICS_LIST, requestData];
|
||||||
|
}, [options?.queryKey, requestData]);
|
||||||
|
|
||||||
|
return useQuery<SuccessResponse<MetricsListResponse> | ErrorResponse, Error>({
|
||||||
|
queryFn: ({ signal }) => getMetricsList(requestData, signal, headers),
|
||||||
|
...options,
|
||||||
|
queryKey,
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,45 @@
|
|||||||
|
import {
|
||||||
|
getMetricsListFilterKeys,
|
||||||
|
MetricsListFilterKeysResponse,
|
||||||
|
} from 'api/metricsExplorer/getMetricsListFilterKeys';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
|
type UseGetMetricsListFilterKeys = (
|
||||||
|
options?: UseQueryOptions<
|
||||||
|
SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>,
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
) => UseQueryResult<
|
||||||
|
SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useGetMetricsListFilterKeys: UseGetMetricsListFilterKeys = (
|
||||||
|
options,
|
||||||
|
headers,
|
||||||
|
) => {
|
||||||
|
const queryKey = useMemo(() => {
|
||||||
|
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||||
|
return [...options.queryKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.queryKey && typeof options.queryKey === 'string') {
|
||||||
|
return options.queryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [REACT_QUERY_KEY.GET_METRICS_LIST_FILTER_KEYS];
|
||||||
|
}, [options?.queryKey]);
|
||||||
|
|
||||||
|
return useQuery<
|
||||||
|
SuccessResponse<MetricsListFilterKeysResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>({
|
||||||
|
queryFn: ({ signal }) => getMetricsListFilterKeys(signal, headers),
|
||||||
|
...options,
|
||||||
|
queryKey,
|
||||||
|
});
|
||||||
|
};
|
50
frontend/src/hooks/metricsExplorer/useGetMetricsTreeMap.ts
Normal file
50
frontend/src/hooks/metricsExplorer/useGetMetricsTreeMap.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
getMetricsTreeMap,
|
||||||
|
MetricsTreeMapPayload,
|
||||||
|
MetricsTreeMapResponse,
|
||||||
|
} from 'api/metricsExplorer/getMetricsTreeMap';
|
||||||
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useQuery, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||||
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
|
||||||
|
type UseGetMetricsTreeMap = (
|
||||||
|
requestData: MetricsTreeMapPayload,
|
||||||
|
|
||||||
|
options?: UseQueryOptions<
|
||||||
|
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>,
|
||||||
|
|
||||||
|
headers?: Record<string, string>,
|
||||||
|
) => UseQueryResult<
|
||||||
|
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useGetMetricsTreeMap: UseGetMetricsTreeMap = (
|
||||||
|
requestData,
|
||||||
|
options,
|
||||||
|
headers,
|
||||||
|
) => {
|
||||||
|
const queryKey = useMemo(() => {
|
||||||
|
if (options?.queryKey && Array.isArray(options.queryKey)) {
|
||||||
|
return [...options.queryKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.queryKey && typeof options.queryKey === 'string') {
|
||||||
|
return options.queryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [REACT_QUERY_KEY.GET_METRICS_TREE_MAP, requestData];
|
||||||
|
}, [options?.queryKey, requestData]);
|
||||||
|
|
||||||
|
return useQuery<
|
||||||
|
SuccessResponse<MetricsTreeMapResponse> | ErrorResponse,
|
||||||
|
Error
|
||||||
|
>({
|
||||||
|
queryFn: ({ signal }) => getMetricsTreeMap(requestData, signal, headers),
|
||||||
|
...options,
|
||||||
|
queryKey,
|
||||||
|
});
|
||||||
|
};
|
@ -31,6 +31,7 @@ export const useAutoComplete = (
|
|||||||
shouldUseSuggestions?: boolean,
|
shouldUseSuggestions?: boolean,
|
||||||
isInfraMonitoring?: boolean,
|
isInfraMonitoring?: boolean,
|
||||||
entity?: K8sCategory | null,
|
entity?: K8sCategory | null,
|
||||||
|
isMetricsExplorer?: boolean,
|
||||||
): IAutoComplete => {
|
): IAutoComplete => {
|
||||||
const [searchValue, setSearchValue] = useState<string>('');
|
const [searchValue, setSearchValue] = useState<string>('');
|
||||||
const [searchKey, setSearchKey] = useState<string>('');
|
const [searchKey, setSearchKey] = useState<string>('');
|
||||||
@ -42,6 +43,7 @@ export const useAutoComplete = (
|
|||||||
shouldUseSuggestions,
|
shouldUseSuggestions,
|
||||||
isInfraMonitoring,
|
isInfraMonitoring,
|
||||||
entity,
|
entity,
|
||||||
|
isMetricsExplorer,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
|
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable sonarjs/cognitive-complexity */
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
|
import { getMetricsListFilterValues } from 'api/metricsExplorer/getMetricsListFilterValues';
|
||||||
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
|
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
|
||||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||||
import {
|
import {
|
||||||
@ -10,6 +11,7 @@ import {
|
|||||||
getTagToken,
|
getTagToken,
|
||||||
isInNInOperator,
|
isInNInOperator,
|
||||||
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||||
|
import { useGetMetricsListFilterKeys } from 'hooks/metricsExplorer/useGetMetricsListFilterKeys';
|
||||||
import useDebounceValue from 'hooks/useDebounce';
|
import useDebounceValue from 'hooks/useDebounce';
|
||||||
import { cloneDeep, isEqual, uniqWith, unset } from 'lodash-es';
|
import { cloneDeep, isEqual, uniqWith, unset } from 'lodash-es';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
@ -50,6 +52,7 @@ export const useFetchKeysAndValues = (
|
|||||||
shouldUseSuggestions?: boolean,
|
shouldUseSuggestions?: boolean,
|
||||||
isInfraMonitoring?: boolean,
|
isInfraMonitoring?: boolean,
|
||||||
entity?: K8sCategory | null,
|
entity?: K8sCategory | null,
|
||||||
|
isMetricsExplorer?: boolean,
|
||||||
): IuseFetchKeysAndValues => {
|
): IuseFetchKeysAndValues => {
|
||||||
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
|
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
|
||||||
const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]);
|
const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]);
|
||||||
@ -98,10 +101,17 @@ export const useFetchKeysAndValues = (
|
|||||||
|
|
||||||
const isQueryEnabled = useMemo(
|
const isQueryEnabled = useMemo(
|
||||||
() =>
|
() =>
|
||||||
query.dataSource === DataSource.METRICS && !isInfraMonitoring
|
query.dataSource === DataSource.METRICS &&
|
||||||
|
!isInfraMonitoring &&
|
||||||
|
!isMetricsExplorer
|
||||||
? !!query.dataSource && !!query.aggregateAttribute.dataType
|
? !!query.dataSource && !!query.aggregateAttribute.dataType
|
||||||
: true,
|
: true,
|
||||||
[isInfraMonitoring, query.aggregateAttribute.dataType, query.dataSource],
|
[
|
||||||
|
isInfraMonitoring,
|
||||||
|
isMetricsExplorer,
|
||||||
|
query.aggregateAttribute.dataType,
|
||||||
|
query.dataSource,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, isFetching, status } = useGetAggregateKeys(
|
const { data, isFetching, status } = useGetAggregateKeys(
|
||||||
@ -139,6 +149,14 @@ export const useFetchKeysAndValues = (
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: metricsListFilterKeysData,
|
||||||
|
isFetching: isFetchingMetricsListFilterKeys,
|
||||||
|
status: fetchingMetricsListFilterKeysStatus,
|
||||||
|
} = useGetMetricsListFilterKeys({
|
||||||
|
enabled: isMetricsExplorer && isQueryEnabled && !shouldUseSuggestions,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches the options to be displayed based on the selected value
|
* Fetches the options to be displayed based on the selected value
|
||||||
* @param value - the selected value
|
* @param value - the selected value
|
||||||
@ -182,6 +200,15 @@ export const useFetchKeysAndValues = (
|
|||||||
: tagValue?.toString() ?? '',
|
: tagValue?.toString() ?? '',
|
||||||
});
|
});
|
||||||
payload = response.payload;
|
payload = response.payload;
|
||||||
|
} else if (isMetricsExplorer) {
|
||||||
|
const response = await getMetricsListFilterValues({
|
||||||
|
searchText: searchKey,
|
||||||
|
filterKey: filterAttributeKey?.key ?? tagKey,
|
||||||
|
filterAttributeKeyDataType:
|
||||||
|
filterAttributeKey?.dataType ?? DataTypes.EMPTY,
|
||||||
|
limit: 10,
|
||||||
|
});
|
||||||
|
payload = response.payload?.data;
|
||||||
} else {
|
} else {
|
||||||
const response = await getAttributesValues({
|
const response = await getAttributesValues({
|
||||||
aggregateOperator: query.aggregateOperator,
|
aggregateOperator: query.aggregateOperator,
|
||||||
@ -238,6 +265,32 @@ export const useFetchKeysAndValues = (
|
|||||||
}
|
}
|
||||||
}, [data?.payload?.attributeKeys, status]);
|
}, [data?.payload?.attributeKeys, status]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
isMetricsExplorer &&
|
||||||
|
fetchingMetricsListFilterKeysStatus === 'success' &&
|
||||||
|
!isFetchingMetricsListFilterKeys &&
|
||||||
|
metricsListFilterKeysData?.payload?.data?.attributeKeys
|
||||||
|
) {
|
||||||
|
setKeys(metricsListFilterKeysData.payload.data.attributeKeys);
|
||||||
|
setSourceKeys((prevState) =>
|
||||||
|
uniqWith(
|
||||||
|
[
|
||||||
|
...(metricsListFilterKeysData.payload.data.attributeKeys ?? []),
|
||||||
|
...prevState,
|
||||||
|
],
|
||||||
|
isEqual,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
metricsListFilterKeysData?.payload?.data?.attributeKeys,
|
||||||
|
fetchingMetricsListFilterKeysStatus,
|
||||||
|
isMetricsExplorer,
|
||||||
|
metricsListFilterKeysData,
|
||||||
|
isFetchingMetricsListFilterKeys,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
fetchingSuggestionsStatus === 'success' &&
|
fetchingSuggestionsStatus === 'success' &&
|
||||||
|
@ -3936,6 +3936,11 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/geojson" "*"
|
"@types/geojson" "*"
|
||||||
|
|
||||||
|
"@types/d3-hierarchy@^1.1.6":
|
||||||
|
version "1.1.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-1.1.11.tgz#c3bd70d025621f73cb3319e97e08ae4c9051c791"
|
||||||
|
integrity sha512-lnQiU7jV+Gyk9oQYk0GGYccuexmQPTp08E0+4BidgFdiJivjEvf+esPSdZqCZ2C7UwTWejWpqetVaU8A+eX3FA==
|
||||||
|
|
||||||
"@types/d3-interpolate@3.0.1", "@types/d3-interpolate@^3.0.0":
|
"@types/d3-interpolate@3.0.1", "@types/d3-interpolate@^3.0.0":
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc"
|
resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz#e7d17fa4a5830ad56fe22ce3b4fac8541a9572dc"
|
||||||
@ -4716,6 +4721,15 @@
|
|||||||
"@types/d3-shape" "^1.3.1"
|
"@types/d3-shape" "^1.3.1"
|
||||||
d3-shape "^1.0.6"
|
d3-shape "^1.0.6"
|
||||||
|
|
||||||
|
"@visx/group@3.12.0":
|
||||||
|
version "3.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@visx/group/-/group-3.12.0.tgz#2c69b810b52f1c1e69bf6f2fe923d184e32078c7"
|
||||||
|
integrity sha512-Dye8iS1alVXPv7nj/7M37gJe6sSKqJLH7x6sEWAsRQ9clI0kFvjbKcKgF+U3aAVQr0NCohheFV+DtR8trfK/Ag==
|
||||||
|
dependencies:
|
||||||
|
"@types/react" "*"
|
||||||
|
classnames "^2.3.1"
|
||||||
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
"@visx/group@3.3.0":
|
"@visx/group@3.3.0":
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/@visx/group/-/group-3.3.0.tgz#20c1b75c1ab31798c3c702b6f58c412c688a6373"
|
resolved "https://registry.yarnpkg.com/@visx/group/-/group-3.3.0.tgz#20c1b75c1ab31798c3c702b6f58c412c688a6373"
|
||||||
@ -4725,6 +4739,18 @@
|
|||||||
classnames "^2.3.1"
|
classnames "^2.3.1"
|
||||||
prop-types "^15.6.2"
|
prop-types "^15.6.2"
|
||||||
|
|
||||||
|
"@visx/hierarchy@3.12.0":
|
||||||
|
version "3.12.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@visx/hierarchy/-/hierarchy-3.12.0.tgz#38295d2469cf957ed6d7700fe968aa16cbb878f0"
|
||||||
|
integrity sha512-+X1HOeLEOODxjAD7ixrWJ4KCVei4wFe8ra3dYU0uZ14RdPPgUeiuyBfdeXWZuAHM6Ix9qrryneatQjkC3h4mvA==
|
||||||
|
dependencies:
|
||||||
|
"@types/d3-hierarchy" "^1.1.6"
|
||||||
|
"@types/react" "*"
|
||||||
|
"@visx/group" "3.12.0"
|
||||||
|
classnames "^2.3.1"
|
||||||
|
d3-hierarchy "^1.1.4"
|
||||||
|
prop-types "^15.6.1"
|
||||||
|
|
||||||
"@visx/scale@3.5.0":
|
"@visx/scale@3.5.0":
|
||||||
version "3.5.0"
|
version "3.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/@visx/scale/-/scale-3.5.0.tgz#c3db3863bbdd24d44781104ef5ee4cdc8df6f11d"
|
resolved "https://registry.yarnpkg.com/@visx/scale/-/scale-3.5.0.tgz#c3db3863bbdd24d44781104ef5ee4cdc8df6f11d"
|
||||||
@ -7095,6 +7121,16 @@ d3-geo@3.1.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
d3-array "2.5.0 - 3"
|
d3-array "2.5.0 - 3"
|
||||||
|
|
||||||
|
d3-hierarchy@3.1.2:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6"
|
||||||
|
integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
|
||||||
|
|
||||||
|
d3-hierarchy@^1.1.4:
|
||||||
|
version "1.1.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83"
|
||||||
|
integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==
|
||||||
|
|
||||||
"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3.0.1:
|
"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
|
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user