mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 20:19:13 +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",
|
||||
"@uiw/react-md-editor": "3.23.5",
|
||||
"@visx/group": "3.3.0",
|
||||
"@visx/hierarchy": "3.12.0",
|
||||
"@visx/shape": "3.5.0",
|
||||
"@visx/tooltip": "3.3.0",
|
||||
"@xstate/react": "^3.0.0",
|
||||
@ -69,6 +70,7 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "5.0.0",
|
||||
"css-minimizer-webpack-plugin": "5.0.1",
|
||||
"d3-hierarchy": "3.1.2",
|
||||
"dayjs": "^1.10.7",
|
||||
"dompurify": "3.1.3",
|
||||
"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_GET_CONNECTION_PARAMS: 'AWS_GET_CONNECTION_PARAMS',
|
||||
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 { 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 { 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 {
|
||||
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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
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,
|
||||
suffixIcon,
|
||||
isInfraMonitoring,
|
||||
isMetricsExplorer,
|
||||
disableNavigationShortcuts,
|
||||
entity,
|
||||
}: QueryBuilderSearchProps): JSX.Element {
|
||||
@ -113,6 +114,7 @@ function QueryBuilderSearch({
|
||||
isLogsExplorerPage,
|
||||
isInfraMonitoring,
|
||||
entity,
|
||||
isMetricsExplorer,
|
||||
);
|
||||
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
@ -129,6 +131,7 @@ function QueryBuilderSearch({
|
||||
isLogsExplorerPage,
|
||||
isInfraMonitoring,
|
||||
entity,
|
||||
isMetricsExplorer,
|
||||
);
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
@ -137,12 +140,12 @@ function QueryBuilderSearch({
|
||||
|
||||
const toggleEditMode = useCallback(
|
||||
(value: boolean) => {
|
||||
// Editing mode is required only in infra monitoring mode
|
||||
if (isInfraMonitoring) {
|
||||
// Editing mode is required only in infra monitoring or metrics explorer
|
||||
if (isInfraMonitoring || isMetricsExplorer) {
|
||||
setIsEditingTag(value);
|
||||
}
|
||||
},
|
||||
[isInfraMonitoring],
|
||||
[isInfraMonitoring, isMetricsExplorer],
|
||||
);
|
||||
|
||||
const onTagRender = ({
|
||||
@ -168,7 +171,7 @@ function QueryBuilderSearch({
|
||||
updateTag(value);
|
||||
// Editing starts
|
||||
toggleEditMode(true);
|
||||
if (isInfraMonitoring) {
|
||||
if (isInfraMonitoring || isMetricsExplorer) {
|
||||
setSearchValue(value);
|
||||
} else {
|
||||
handleSearch(value);
|
||||
@ -240,8 +243,11 @@ function QueryBuilderSearch({
|
||||
);
|
||||
|
||||
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 => {
|
||||
@ -291,8 +297,8 @@ function QueryBuilderSearch({
|
||||
};
|
||||
});
|
||||
|
||||
// If in infra monitoring, only run the onChange query when editing is finsished.
|
||||
if (isInfraMonitoring) {
|
||||
// If in infra monitoring or metrics explorer, only run the onChange query when editing is finsished.
|
||||
if (isInfraMonitoring || isMetricsExplorer) {
|
||||
if (!isEditingTag) {
|
||||
onChange(initialTagFilters);
|
||||
}
|
||||
@ -498,6 +504,7 @@ interface QueryBuilderSearchProps {
|
||||
isInfraMonitoring?: boolean;
|
||||
disableNavigationShortcuts?: boolean;
|
||||
entity?: K8sCategory | null;
|
||||
isMetricsExplorer?: boolean;
|
||||
}
|
||||
|
||||
QueryBuilderSearch.defaultProps = {
|
||||
@ -508,6 +515,7 @@ QueryBuilderSearch.defaultProps = {
|
||||
isInfraMonitoring: false,
|
||||
disableNavigationShortcuts: false,
|
||||
entity: null,
|
||||
isMetricsExplorer: false,
|
||||
};
|
||||
|
||||
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,
|
||||
isInfraMonitoring?: boolean,
|
||||
entity?: K8sCategory | null,
|
||||
isMetricsExplorer?: boolean,
|
||||
): IAutoComplete => {
|
||||
const [searchValue, setSearchValue] = useState<string>('');
|
||||
const [searchKey, setSearchKey] = useState<string>('');
|
||||
@ -42,6 +43,7 @@ export const useAutoComplete = (
|
||||
shouldUseSuggestions,
|
||||
isInfraMonitoring,
|
||||
entity,
|
||||
isMetricsExplorer,
|
||||
);
|
||||
|
||||
const [key, operator, result] = useSetCurrentKeyAndOperator(searchValue, keys);
|
||||
|
@ -1,4 +1,5 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { getMetricsListFilterValues } from 'api/metricsExplorer/getMetricsListFilterValues';
|
||||
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import {
|
||||
@ -10,6 +11,7 @@ import {
|
||||
getTagToken,
|
||||
isInNInOperator,
|
||||
} from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { useGetMetricsListFilterKeys } from 'hooks/metricsExplorer/useGetMetricsListFilterKeys';
|
||||
import useDebounceValue from 'hooks/useDebounce';
|
||||
import { cloneDeep, isEqual, uniqWith, unset } from 'lodash-es';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
@ -50,6 +52,7 @@ export const useFetchKeysAndValues = (
|
||||
shouldUseSuggestions?: boolean,
|
||||
isInfraMonitoring?: boolean,
|
||||
entity?: K8sCategory | null,
|
||||
isMetricsExplorer?: boolean,
|
||||
): IuseFetchKeysAndValues => {
|
||||
const [keys, setKeys] = useState<BaseAutocompleteData[]>([]);
|
||||
const [exampleQueries, setExampleQueries] = useState<TagFilter[]>([]);
|
||||
@ -98,10 +101,17 @@ export const useFetchKeysAndValues = (
|
||||
|
||||
const isQueryEnabled = useMemo(
|
||||
() =>
|
||||
query.dataSource === DataSource.METRICS && !isInfraMonitoring
|
||||
query.dataSource === DataSource.METRICS &&
|
||||
!isInfraMonitoring &&
|
||||
!isMetricsExplorer
|
||||
? !!query.dataSource && !!query.aggregateAttribute.dataType
|
||||
: true,
|
||||
[isInfraMonitoring, query.aggregateAttribute.dataType, query.dataSource],
|
||||
[
|
||||
isInfraMonitoring,
|
||||
isMetricsExplorer,
|
||||
query.aggregateAttribute.dataType,
|
||||
query.dataSource,
|
||||
],
|
||||
);
|
||||
|
||||
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
|
||||
* @param value - the selected value
|
||||
@ -182,6 +200,15 @@ export const useFetchKeysAndValues = (
|
||||
: tagValue?.toString() ?? '',
|
||||
});
|
||||
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 {
|
||||
const response = await getAttributesValues({
|
||||
aggregateOperator: query.aggregateOperator,
|
||||
@ -238,6 +265,32 @@ export const useFetchKeysAndValues = (
|
||||
}
|
||||
}, [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(() => {
|
||||
if (
|
||||
fetchingSuggestionsStatus === 'success' &&
|
||||
|
@ -3936,6 +3936,11 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "3.0.1"
|
||||
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"
|
||||
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":
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/group/-/group-3.3.0.tgz#20c1b75c1ab31798c3c702b6f58c412c688a6373"
|
||||
@ -4725,6 +4739,18 @@
|
||||
classnames "^2.3.1"
|
||||
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":
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@visx/scale/-/scale-3.5.0.tgz#c3db3863bbdd24d44781104ef5ee4cdc8df6f11d"
|
||||
@ -7095,6 +7121,16 @@ d3-geo@3.1.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
|
||||
|
Loading…
x
Reference in New Issue
Block a user