mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-26 10:22:35 +00:00
Compare commits
6 Commits
feat/updat
...
chore/issu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db87b30216 | ||
|
|
dfaa09ff8a | ||
|
|
903e2f6e33 | ||
|
|
be8bc019d0 | ||
|
|
7a85ee1602 | ||
|
|
4b0cbb787a |
@@ -320,4 +320,3 @@ user:
|
||||
# The name of the organization to create or look up for the root user.
|
||||
org:
|
||||
name: default
|
||||
id: 00000000-0000-0000-0000-000000000000
|
||||
|
||||
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.113.0
|
||||
image: signoz/signoz:v0.112.1
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
@@ -213,7 +213,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.144.1
|
||||
image: signoz/signoz-otel-collector:v0.142.1
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
@@ -241,7 +241,7 @@ services:
|
||||
replicas: 3
|
||||
signoz-telemetrystore-migrator:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.144.1
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||
environment:
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.113.0
|
||||
image: signoz/signoz:v0.112.1
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
@@ -139,7 +139,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.144.1
|
||||
image: signoz/signoz-otel-collector:v0.142.1
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
command:
|
||||
@@ -167,7 +167,7 @@ services:
|
||||
replicas: 3
|
||||
signoz-telemetrystore-migrator:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:v0.144.1
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||
environment:
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_CLUSTER=cluster
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.113.0}
|
||||
image: signoz/signoz:${VERSION:-v0.112.1}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
@@ -204,7 +204,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.1}
|
||||
container_name: signoz-otel-collector
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
@@ -229,7 +229,7 @@ services:
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
signoz-telemetrystore-migrator:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||
container_name: signoz-telemetrystore-migrator
|
||||
environment:
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.113.0}
|
||||
image: signoz/signoz:${VERSION:-v0.112.1}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
@@ -132,7 +132,7 @@ services:
|
||||
retries: 3
|
||||
otel-collector:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.1}
|
||||
container_name: signoz-otel-collector
|
||||
entrypoint:
|
||||
- /bin/sh
|
||||
@@ -157,7 +157,7 @@ services:
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
signoz-telemetrystore-migrator:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.144.1}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-v0.142.0}
|
||||
container_name: signoz-telemetrystore-migrator
|
||||
environment:
|
||||
- SIGNOZ_OTEL_COLLECTOR_CLICKHOUSE_DSN=tcp://clickhouse:9000
|
||||
|
||||
@@ -842,17 +842,6 @@ components:
|
||||
- temporality
|
||||
- isMonotonic
|
||||
type: object
|
||||
MetrictypesComparisonSpaceAggregationParam:
|
||||
properties:
|
||||
operator:
|
||||
type: string
|
||||
threshold:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- operator
|
||||
- threshold
|
||||
type: object
|
||||
MetrictypesSpaceAggregation:
|
||||
enum:
|
||||
- sum
|
||||
@@ -1149,8 +1138,6 @@ components:
|
||||
type: object
|
||||
Querybuildertypesv5MetricAggregation:
|
||||
properties:
|
||||
comparisonSpaceAggregationParam:
|
||||
$ref: '#/components/schemas/MetrictypesComparisonSpaceAggregationParam'
|
||||
metricName:
|
||||
type: string
|
||||
reduceTo:
|
||||
|
||||
@@ -1006,18 +1006,6 @@ export interface MetricsexplorertypesUpdateMetricMetadataRequestDTO {
|
||||
unit: string;
|
||||
}
|
||||
|
||||
export interface MetrictypesComparisonSpaceAggregationParamDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
operator: string;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
threshold: number;
|
||||
}
|
||||
|
||||
export enum MetrictypesSpaceAggregationDTO {
|
||||
sum = 'sum',
|
||||
avg = 'avg',
|
||||
@@ -1379,7 +1367,6 @@ export interface Querybuildertypesv5LogAggregationDTO {
|
||||
}
|
||||
|
||||
export interface Querybuildertypesv5MetricAggregationDTO {
|
||||
comparisonSpaceAggregationParam?: MetrictypesComparisonSpaceAggregationParamDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
|
||||
@@ -50,7 +50,6 @@ export interface HostListResponse {
|
||||
total: number;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingK8SAgentMetrics: boolean;
|
||||
endTimeBeforeRetention: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
26
frontend/src/api/metricsExplorer/v2/getMetricMetadata.ts
Normal file
26
frontend/src/api/metricsExplorer/v2/getMetricMetadata.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { ApiV2Instance as axios } from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponseV2, ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { MetricMetadataResponse } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
||||
|
||||
export const getMetricMetadata = async (
|
||||
metricName: string,
|
||||
signal?: AbortSignal,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<SuccessResponseV2<MetricMetadataResponse> | ErrorResponseV2> => {
|
||||
try {
|
||||
const encodedMetricName = encodeURIComponent(metricName);
|
||||
const response = await axios.get(`/metrics/${encodedMetricName}/metadata`, {
|
||||
signal,
|
||||
headers,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
@@ -86,13 +86,8 @@ function LogDetailInner({
|
||||
const handleClickOutside = (e: MouseEvent): void => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Don't close if clicking on drawer content, overlays, or portal elements
|
||||
if (
|
||||
target.closest('[data-log-detail-ignore="true"]') ||
|
||||
target.closest('.cm-tooltip-autocomplete') ||
|
||||
target.closest('.drawer-popover') ||
|
||||
target.closest('.query-status-popover')
|
||||
) {
|
||||
// Don't close if clicking on explicitly ignored regions
|
||||
if (target.closest('[data-log-detail-ignore="true"]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -405,11 +400,7 @@ function LogDetailInner({
|
||||
<div className="log-detail-drawer__content" data-log-detail-ignore="true">
|
||||
<div className="log-detail-drawer__log">
|
||||
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
||||
<Tooltip
|
||||
title={removeEscapeCharacters(log?.body)}
|
||||
placement="left"
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Tooltip title={removeEscapeCharacters(log?.body)} placement="left">
|
||||
<div className="log-body" dangerouslySetInnerHTML={htmlBody} />
|
||||
</Tooltip>
|
||||
|
||||
@@ -475,7 +466,6 @@ function LogDetailInner({
|
||||
title="Show Filters"
|
||||
placement="topLeft"
|
||||
aria-label="Show Filters"
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
className="action-btn"
|
||||
@@ -491,7 +481,6 @@ function LogDetailInner({
|
||||
aria-label={
|
||||
selectedView === VIEW_TYPES.JSON ? 'Copy JSON' : 'Copy Log Link'
|
||||
}
|
||||
mouseLeaveDelay={0}
|
||||
>
|
||||
<Button
|
||||
className="action-btn"
|
||||
|
||||
@@ -27,11 +27,7 @@ function AddToQueryHOC({
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||
<div className={cx('addToQueryContainer', fontSize)} onClick={handleQueryAdd}>
|
||||
<Popover
|
||||
overlayClassName="drawer-popover"
|
||||
placement="top"
|
||||
content={popOverContent}
|
||||
>
|
||||
<Popover placement="top" content={popOverContent}>
|
||||
{children}
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,6 @@ function CopyClipboardHOC({
|
||||
<span onClick={onClick} role="presentation" tabIndex={-1}>
|
||||
<Popover
|
||||
placement="top"
|
||||
overlayClassName="drawer-popover"
|
||||
content={<span style={{ fontSize: '0.9rem' }}>{tooltipText}</span>}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
startCompletion,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { copilot } from '@uiw/codemirror-theme-copilot';
|
||||
import { githubLight } from '@uiw/codemirror-theme-github';
|
||||
@@ -563,7 +564,15 @@ function QuerySearch({
|
||||
const lastPos = lastPosRef.current;
|
||||
|
||||
if (newPos.line !== lastPos.line || newPos.ch !== lastPos.ch) {
|
||||
setCursorPos(newPos);
|
||||
setCursorPos((lastPos) => {
|
||||
if (newPos.ch !== lastPos.ch && newPos.ch === 0) {
|
||||
Sentry.captureEvent({
|
||||
message: `Cursor jumped to start of line from ${lastPos.ch} to ${newPos.ch}`,
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
return newPos;
|
||||
});
|
||||
lastPosRef.current = newPos;
|
||||
|
||||
if (doc) {
|
||||
@@ -1319,10 +1328,7 @@ function QuerySearch({
|
||||
)}
|
||||
|
||||
<div className="query-where-clause-editor-container">
|
||||
<Tooltip
|
||||
title={<div data-log-detail-ignore="true">{getTooltipContent()}</div>}
|
||||
placement="left"
|
||||
>
|
||||
<Tooltip title={getTooltipContent()} placement="left">
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/search-syntax/"
|
||||
target="_blank"
|
||||
|
||||
@@ -40,7 +40,6 @@ function ValueGraph({
|
||||
value,
|
||||
rawValue,
|
||||
thresholds,
|
||||
yAxisUnit,
|
||||
}: ValueGraphProps): JSX.Element {
|
||||
const { t } = useTranslation(['valueGraph']);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -88,7 +87,7 @@ function ValueGraph({
|
||||
const {
|
||||
threshold,
|
||||
isConflictingThresholds,
|
||||
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue, yAxisUnit);
|
||||
} = getBackgroundColorAndThresholdCheck(thresholds, rawValue);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -156,7 +155,6 @@ interface ValueGraphProps {
|
||||
value: string;
|
||||
rawValue: number;
|
||||
thresholds: ThresholdProps[];
|
||||
yAxisUnit?: string;
|
||||
}
|
||||
|
||||
export default ValueGraph;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { evaluateThresholdWithConvertedValue } from 'container/GridTableComponent/utils';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
|
||||
function doesValueSatisfyThreshold(
|
||||
function compareThreshold(
|
||||
rawValue: number,
|
||||
threshold: ThresholdProps,
|
||||
yAxisUnit?: string,
|
||||
): boolean {
|
||||
if (
|
||||
threshold.thresholdOperator === undefined ||
|
||||
@@ -12,14 +11,31 @@ function doesValueSatisfyThreshold(
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
switch (threshold.thresholdOperator) {
|
||||
case '>':
|
||||
return rawValue > threshold.thresholdValue;
|
||||
case '>=':
|
||||
return rawValue >= threshold.thresholdValue;
|
||||
case '<':
|
||||
return rawValue < threshold.thresholdValue;
|
||||
case '<=':
|
||||
return rawValue <= threshold.thresholdValue;
|
||||
case '=':
|
||||
return rawValue === threshold.thresholdValue;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return evaluateThresholdWithConvertedValue(
|
||||
rawValue,
|
||||
threshold.thresholdValue,
|
||||
threshold.thresholdOperator,
|
||||
threshold.thresholdUnit,
|
||||
yAxisUnit,
|
||||
);
|
||||
function extractNumbersFromString(inputString: string): number[] {
|
||||
const regex = /[+-]?\d+(\.\d+)?/g;
|
||||
const matches = inputString.match(regex);
|
||||
|
||||
if (matches) {
|
||||
return matches.map(Number);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
function getHighestPrecedenceThreshold(
|
||||
@@ -44,32 +60,21 @@ function getHighestPrecedenceThreshold(
|
||||
return highestPrecedenceThreshold;
|
||||
}
|
||||
|
||||
function extractNumbersFromString(inputString: string): number[] {
|
||||
const regex = /[+-]?\d+(\.\d+)?/g;
|
||||
const matches = inputString.match(regex);
|
||||
|
||||
if (matches) {
|
||||
return matches.map(Number);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getBackgroundColorAndThresholdCheck(
|
||||
thresholds: ThresholdProps[],
|
||||
rawValue: number,
|
||||
yAxisUnit?: string,
|
||||
): {
|
||||
threshold: ThresholdProps;
|
||||
isConflictingThresholds: boolean;
|
||||
} {
|
||||
const matchingThresholds = thresholds.filter((threshold) => {
|
||||
const numbers = extractNumbersFromString(rawValue.toString());
|
||||
if (numbers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return doesValueSatisfyThreshold(numbers[0], threshold, yAxisUnit);
|
||||
});
|
||||
const matchingThresholds = thresholds.filter((threshold) =>
|
||||
compareThreshold(
|
||||
extractNumbersFromString(
|
||||
getYAxisFormattedValue(rawValue.toString(), threshold.thresholdUnit || ''),
|
||||
)[0],
|
||||
threshold,
|
||||
),
|
||||
);
|
||||
|
||||
if (matchingThresholds.length === 0) {
|
||||
return {
|
||||
|
||||
@@ -22,8 +22,6 @@ function YAxisUnitSelector({
|
||||
'data-testid': dataTestId,
|
||||
source,
|
||||
initialValue,
|
||||
categoriesOverride,
|
||||
containerClassName,
|
||||
}: YAxisUnitSelectorProps): JSX.Element {
|
||||
const universalUnit = mapMetricUnitToUniversalUnit(value);
|
||||
|
||||
@@ -68,14 +66,10 @@ function YAxisUnitSelector({
|
||||
return aliases.some((alias) => alias.toLowerCase().includes(search));
|
||||
};
|
||||
|
||||
const categoriesToRender = useMemo(() => {
|
||||
return categoriesOverride || getYAxisCategories(source);
|
||||
}, [categoriesOverride, source]);
|
||||
const categories = getYAxisCategories(source);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('y-axis-unit-selector-component', containerClassName)}
|
||||
>
|
||||
<div className="y-axis-unit-selector-component">
|
||||
<Select
|
||||
showSearch
|
||||
value={universalUnit}
|
||||
@@ -96,7 +90,7 @@ function YAxisUnitSelector({
|
||||
data-testid={dataTestId}
|
||||
allowClear
|
||||
>
|
||||
{categoriesToRender.map((category) => (
|
||||
{categories.map((category) => (
|
||||
<Select.OptGroup key={category.name} label={category.name}>
|
||||
{category.units.map((unit) => (
|
||||
<Select.Option key={unit.id} value={unit.id}>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { YAxisCategoryNames } from '../constants';
|
||||
import { UniversalYAxisUnit, YAxisSource } from '../types';
|
||||
import { YAxisSource } from '../types';
|
||||
import YAxisUnitSelector from '../YAxisUnitSelector';
|
||||
|
||||
describe('YAxisUnitSelector', () => {
|
||||
@@ -124,34 +123,4 @@ describe('YAxisUnitSelector', () => {
|
||||
const warningIcon = screen.queryByLabelText('warning');
|
||||
expect(warningIcon).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses categories override to render custom units', () => {
|
||||
const customCategories = [
|
||||
{
|
||||
name: YAxisCategoryNames.Data,
|
||||
units: [
|
||||
{
|
||||
id: UniversalYAxisUnit.BYTES,
|
||||
name: 'Custom Bytes (B)',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
render(
|
||||
<YAxisUnitSelector
|
||||
value=""
|
||||
onChange={mockOnChange}
|
||||
source={YAxisSource.ALERTS}
|
||||
categoriesOverride={customCategories}
|
||||
/>,
|
||||
);
|
||||
|
||||
const select = screen.getByRole('combobox');
|
||||
|
||||
fireEvent.mouseDown(select);
|
||||
|
||||
expect(screen.getByText('Custom Bytes (B)')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Bytes (B)')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,8 +9,6 @@ export interface YAxisUnitSelectorProps {
|
||||
'data-testid'?: string;
|
||||
source: YAxisSource;
|
||||
initialValue?: string;
|
||||
categoriesOverride?: YAxisCategory[];
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
export enum UniversalYAxisUnit {
|
||||
|
||||
@@ -117,7 +117,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
const [showSlowApiWarning, setShowSlowApiWarning] = useState(false);
|
||||
const [slowApiWarningShown, setSlowApiWarningShown] = useState(false);
|
||||
|
||||
const { currentVersion } = useSelector<AppState, AppReducer>(
|
||||
const { latestVersion } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
@@ -213,9 +213,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
},
|
||||
{
|
||||
queryFn: (): Promise<SuccessResponse<ChangelogSchema> | ErrorResponse> =>
|
||||
getChangelogByVersion(currentVersion, changelogForTenant),
|
||||
queryKey: ['getChangelogByVersion', currentVersion, changelogForTenant],
|
||||
enabled: isLoggedIn && Boolean(currentVersion),
|
||||
getChangelogByVersion(latestVersion, changelogForTenant),
|
||||
queryKey: ['getChangelogByVersion', latestVersion, changelogForTenant],
|
||||
enabled: isLoggedIn && Boolean(latestVersion),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -226,7 +226,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
!changelog &&
|
||||
!getChangelogByVersionResponse.isLoading &&
|
||||
isLoggedIn &&
|
||||
Boolean(currentVersion)
|
||||
Boolean(latestVersion)
|
||||
) {
|
||||
getChangelogByVersionResponse.refetch();
|
||||
}
|
||||
@@ -237,9 +237,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
if (
|
||||
isCloudUserVal &&
|
||||
Boolean(currentVersion) &&
|
||||
Boolean(latestVersion) &&
|
||||
seenChangelogVersion != null &&
|
||||
currentVersion !== seenChangelogVersion &&
|
||||
latestVersion !== seenChangelogVersion &&
|
||||
daysSinceAccountCreation > MIN_ACCOUNT_AGE_FOR_CHANGELOG && // Show to only users older than 2 weeks
|
||||
!isWorkspaceAccessRestricted
|
||||
) {
|
||||
@@ -255,7 +255,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
isCloudUserVal,
|
||||
currentVersion,
|
||||
latestVersion,
|
||||
seenChangelogVersion,
|
||||
toggleChangelogModal,
|
||||
isWorkspaceAccessRestricted,
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
|
||||
import { getNonIntegrationDashboardById } from 'mocks-server/__mockdata__/dashboards';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
|
||||
import { fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'tests/test-utils';
|
||||
|
||||
import DashboardDescription from '..';
|
||||
|
||||
@@ -17,6 +25,7 @@ const DASHBOARD_TITLE_TEXT = 'thor';
|
||||
const DASHBOARD_PATH = '/dashboard/4';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
const mockNavigate = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
@@ -28,6 +37,11 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
useNavigate: (): jest.Mock => mockNavigate,
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/TopNav/DateTimeSelectionV2/index.tsx',
|
||||
() =>
|
||||
@@ -45,6 +59,8 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
||||
describe('Dashboard landing page actions header tests', () => {
|
||||
beforeEach(() => {
|
||||
mockSafeNavigate.mockClear();
|
||||
mockNavigate.mockClear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
it('unlock dashboard should be disabled for integrations created dashboards', async () => {
|
||||
@@ -124,17 +140,26 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
await waitFor(() => expect(lockUnlockButton).not.toBeDisabled());
|
||||
});
|
||||
|
||||
it('should navigate to dashboard list with correct params and exclude variables', async () => {
|
||||
const dashboardUrlWithVariables = `${DASHBOARD_PATH}?variables=%7B%22var1%22%3A%22value1%22%7D&otherParam=test`;
|
||||
it('should navigate to the dashboard list with stored query params when clicking the dashboard breadcrumb', async () => {
|
||||
const storedParams = JSON.stringify({
|
||||
columnKey: 'createdAt',
|
||||
order: 'ascend',
|
||||
page: '2',
|
||||
search: 'test',
|
||||
});
|
||||
sessionStorage.setItem(
|
||||
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
|
||||
storedParams,
|
||||
);
|
||||
|
||||
const mockLocation = {
|
||||
pathname: DASHBOARD_PATH,
|
||||
search: '?variables=%7B%22var1%22%3A%22value1%22%7D&otherParam=test',
|
||||
search: '',
|
||||
};
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
|
||||
const { getByText } = render(
|
||||
<MemoryRouter initialEntries={[dashboardUrlWithVariables]}>
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardProvider>
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
@@ -154,27 +179,49 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
),
|
||||
);
|
||||
|
||||
// Click the dashboard breadcrumb to navigate back to list
|
||||
const dashboardButton = getByText('Dashboard /');
|
||||
fireEvent.click(dashboardButton);
|
||||
await userEvent.click(dashboardButton);
|
||||
|
||||
// Verify navigation was called with correct URL
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
'/dashboard?columnKey=updatedAt&order=descend&page=1&search=',
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith({
|
||||
pathname: ROUTES.ALL_DASHBOARD,
|
||||
search: `?${storedParams}`,
|
||||
});
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should navigate to dashboard list page when there are no stored query params', async () => {
|
||||
const mockLocation = {
|
||||
pathname: DASHBOARD_PATH,
|
||||
search: '',
|
||||
};
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
|
||||
const { getByText } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardProvider>
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
enter: (): Promise<void> => Promise.resolve(),
|
||||
exit: (): Promise<void> => Promise.resolve(),
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardProvider>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
// Ensure the URL contains only essential dashboard list params
|
||||
const calledUrl = mockSafeNavigate.mock.calls[0][0] as string;
|
||||
const urlParams = new URLSearchParams(calledUrl.split('?')[1]);
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId(DASHBOARD_TEST_ID)).toHaveTextContent(
|
||||
DASHBOARD_TITLE_TEXT,
|
||||
),
|
||||
);
|
||||
|
||||
// Should have essential dashboard list params
|
||||
expect(urlParams.get('columnKey')).toBe('updatedAt');
|
||||
expect(urlParams.get('order')).toBe('descend');
|
||||
expect(urlParams.get('page')).toBe('1');
|
||||
expect(urlParams.get('search')).toBe('');
|
||||
const dashboardButton = getByText('Dashboard /');
|
||||
await userEvent.click(dashboardButton);
|
||||
|
||||
// Should NOT have variables or other dashboard-specific params
|
||||
expect(urlParams.has('variables')).toBeFalsy();
|
||||
expect(urlParams.has('relativeTime')).toBeFalsy();
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(ROUTES.ALL_DASHBOARD);
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Button } from 'antd';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
@@ -12,7 +13,7 @@ import './DashboardBreadcrumbs.styles.scss';
|
||||
|
||||
function DashboardBreadcrumbs(): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { selectedDashboard, listSortOrder } = useDashboard();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
|
||||
const selectedData = selectedDashboard
|
||||
? {
|
||||
@@ -24,15 +25,24 @@ function DashboardBreadcrumbs(): JSX.Element {
|
||||
const { title = '', image = Base64Icons[0] } = selectedData || {};
|
||||
|
||||
const goToListPage = useCallback(() => {
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set('columnKey', listSortOrder.columnKey as string);
|
||||
urlParams.set('order', listSortOrder.order as string);
|
||||
urlParams.set('page', listSortOrder.pagination as string);
|
||||
urlParams.set('search', listSortOrder.search as string);
|
||||
const dashboardsListQueryParamsObject = sessionStorage.getItem(
|
||||
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
|
||||
);
|
||||
|
||||
const generatedUrl = `${ROUTES.ALL_DASHBOARD}?${urlParams.toString()}`;
|
||||
safeNavigate(generatedUrl);
|
||||
}, [listSortOrder, safeNavigate]);
|
||||
if (dashboardsListQueryParamsObject) {
|
||||
const queryParamsString = Object.entries(
|
||||
JSON.parse(dashboardsListQueryParamsObject),
|
||||
)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('&');
|
||||
safeNavigate({
|
||||
pathname: ROUTES.ALL_DASHBOARD,
|
||||
search: `?${queryParamsString}`,
|
||||
});
|
||||
} else {
|
||||
safeNavigate(ROUTES.ALL_DASHBOARD);
|
||||
}
|
||||
}, [safeNavigate]);
|
||||
|
||||
return (
|
||||
<div className="dashboard-breadcrumbs">
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useFullScreenHandle } from 'react-full-screen';
|
||||
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
|
||||
|
||||
import Description from './DashboardDescription';
|
||||
import GridGraphs from './GridGraphs';
|
||||
|
||||
function DashboardContainer(): JSX.Element {
|
||||
const handle = useFullScreenHandle();
|
||||
|
||||
useEffect(() => {
|
||||
return (): void => {
|
||||
sessionStorage.removeItem(DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Description handle={handle} />
|
||||
|
||||
@@ -49,7 +49,7 @@ function evaluateCondition(
|
||||
* @param columnUnit - The current unit of the value.
|
||||
* @returns A boolean indicating whether the value meets the threshold condition.
|
||||
*/
|
||||
export function evaluateThresholdWithConvertedValue(
|
||||
function evaluateThresholdWithConvertedValue(
|
||||
value: number,
|
||||
thresholdValue: number,
|
||||
thresholdOperator?: string,
|
||||
|
||||
@@ -99,7 +99,6 @@ function GridValueComponent({
|
||||
<ValueGraph
|
||||
thresholds={thresholds || []}
|
||||
rawValue={value}
|
||||
yAxisUnit={yAxisUnit}
|
||||
value={
|
||||
yAxisUnit
|
||||
? getYAxisFormattedValue(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Skeleton,
|
||||
@@ -14,93 +14,12 @@ import { InfraMonitoringEvents } from 'constants/events';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
||||
import {
|
||||
EmptyOrLoadingViewProps,
|
||||
formatDataForTable,
|
||||
getHostsListColumns,
|
||||
HostRowData,
|
||||
HostsListTableProps,
|
||||
} from './utils';
|
||||
|
||||
function EmptyOrLoadingView(
|
||||
viewState: EmptyOrLoadingViewProps,
|
||||
): React.ReactNode {
|
||||
const { isError, errorMessage } = viewState;
|
||||
if (isError) {
|
||||
return <Typography>{errorMessage || 'Something went wrong'}</Typography>;
|
||||
}
|
||||
if (viewState.showHostsEmptyState) {
|
||||
return (
|
||||
<HostsEmptyOrIncorrectMetrics
|
||||
noData={!viewState.sentAnyHostMetricsData}
|
||||
incorrectData={viewState.isSendingIncorrectK8SAgentMetrics}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (viewState.showEndTimeBeforeRetentionMessage) {
|
||||
return (
|
||||
<div className="hosts-empty-state-container">
|
||||
<div className="hosts-empty-state-container-content">
|
||||
<img className="eyes-emoji" src="/Images/eyesEmoji.svg" alt="eyes emoji" />
|
||||
<div className="no-hosts-message">
|
||||
<Typography.Title level={5} className="no-hosts-message-title">
|
||||
Queried time range is before earliest host metrics
|
||||
</Typography.Title>
|
||||
<Typography.Text className="no-hosts-message-text">
|
||||
Your requested end time is earlier than the earliest detected time of
|
||||
host metrics data, please adjust your end time.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (viewState.showNoRecordsInSelectedTimeRangeMessage) {
|
||||
return (
|
||||
<div className="no-filtered-hosts-message-container">
|
||||
<div className="no-filtered-hosts-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
<Typography.Title level={5} className="no-filtered-hosts-title">
|
||||
No host metrics found
|
||||
</Typography.Title>
|
||||
<Typography.Text className="no-filtered-hosts-message">
|
||||
No host metrics in the selected time range and filters. Please adjust your
|
||||
time range or filters.
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (viewState.showTableLoadingState) {
|
||||
return (
|
||||
<div className="hosts-list-loading-state">
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function HostsListTable({
|
||||
isLoading,
|
||||
isFetching,
|
||||
@@ -127,11 +46,6 @@ export default function HostsListTable({
|
||||
[data],
|
||||
);
|
||||
|
||||
const endTimeBeforeRetention = useMemo(
|
||||
() => data?.payload?.data?.endTimeBeforeRetention || false,
|
||||
[data],
|
||||
);
|
||||
|
||||
const formattedHostMetricsData = useMemo(
|
||||
() => formatDataForTable(hostMetricsData),
|
||||
[hostMetricsData],
|
||||
@@ -170,6 +84,12 @@ export default function HostsListTable({
|
||||
});
|
||||
};
|
||||
|
||||
const showNoFilteredHostsMessage =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
filters.items.length > 0;
|
||||
|
||||
const showHostsEmptyState =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
@@ -177,36 +97,63 @@ export default function HostsListTable({
|
||||
(!sentAnyHostMetricsData || isSendingIncorrectK8SAgentMetrics) &&
|
||||
!filters.items.length;
|
||||
|
||||
const showEndTimeBeforeRetentionMessage =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
endTimeBeforeRetention &&
|
||||
!filters.items.length;
|
||||
|
||||
const showNoRecordsInSelectedTimeRangeMessage =
|
||||
!isFetching &&
|
||||
!isLoading &&
|
||||
formattedHostMetricsData.length === 0 &&
|
||||
!showEndTimeBeforeRetentionMessage &&
|
||||
!showHostsEmptyState;
|
||||
|
||||
const showTableLoadingState =
|
||||
(isLoading || isFetching) && formattedHostMetricsData.length === 0;
|
||||
|
||||
const emptyOrLoadingView = EmptyOrLoadingView({
|
||||
isError,
|
||||
errorMessage: data?.error ?? '',
|
||||
showHostsEmptyState,
|
||||
sentAnyHostMetricsData,
|
||||
isSendingIncorrectK8SAgentMetrics,
|
||||
showEndTimeBeforeRetentionMessage,
|
||||
showNoRecordsInSelectedTimeRangeMessage,
|
||||
showTableLoadingState,
|
||||
});
|
||||
if (isError) {
|
||||
return <Typography>{data?.error || 'Something went wrong'}</Typography>;
|
||||
}
|
||||
|
||||
if (emptyOrLoadingView) {
|
||||
return <>{emptyOrLoadingView}</>;
|
||||
if (showHostsEmptyState) {
|
||||
return (
|
||||
<HostsEmptyOrIncorrectMetrics
|
||||
noData={!sentAnyHostMetricsData}
|
||||
incorrectData={isSendingIncorrectK8SAgentMetrics}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (showNoFilteredHostsMessage) {
|
||||
return (
|
||||
<div className="no-filtered-hosts-message-container">
|
||||
<div className="no-filtered-hosts-message-content">
|
||||
<img
|
||||
src="/Icons/emptyState.svg"
|
||||
alt="thinking-emoji"
|
||||
className="empty-state-svg"
|
||||
/>
|
||||
|
||||
<Typography.Text className="no-filtered-hosts-message">
|
||||
This query had no results. Edit your query and try again!
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (showTableLoadingState) {
|
||||
return (
|
||||
<div className="hosts-list-loading-state">
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
<Skeleton.Input
|
||||
className="hosts-list-loading-state-item"
|
||||
size="large"
|
||||
block
|
||||
active
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { HostData, HostListResponse } from 'api/infraMonitoring/getHostLists';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
import HostsListTable from '../HostsListTable';
|
||||
import { HostsListTableProps } from '../utils';
|
||||
|
||||
const EMPTY_STATE_CONTAINER_CLASS = '.hosts-empty-state-container';
|
||||
|
||||
const createMockHost = (): HostData =>
|
||||
({
|
||||
describe('HostsListTable', () => {
|
||||
const mockHost = {
|
||||
hostName: 'test-host-1',
|
||||
active: true,
|
||||
cpu: 0.75,
|
||||
@@ -18,46 +14,20 @@ const createMockHost = (): HostData =>
|
||||
wait: 0.03,
|
||||
load15: 1.5,
|
||||
os: 'linux',
|
||||
cpuTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
memoryTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
waitTimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
load15TimeSeries: { labels: {}, labelsArray: [], values: [] },
|
||||
} as HostData);
|
||||
};
|
||||
|
||||
const createMockTableData = (
|
||||
overrides: Partial<HostListResponse['data']> = {},
|
||||
): SuccessResponse<HostListResponse> => {
|
||||
const mockHost = createMockHost();
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: 'Success',
|
||||
error: null,
|
||||
const mockTableData = {
|
||||
payload: {
|
||||
status: 'success',
|
||||
data: {
|
||||
type: 'list',
|
||||
records: [mockHost],
|
||||
groups: null,
|
||||
total: 1,
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
endTimeBeforeRetention: false,
|
||||
...overrides,
|
||||
hosts: [mockHost],
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('HostsListTable', () => {
|
||||
const mockHost = createMockHost();
|
||||
const mockTableData = createMockTableData();
|
||||
|
||||
const mockOnHostClick = jest.fn();
|
||||
const mockSetCurrentPage = jest.fn();
|
||||
const mockSetOrderBy = jest.fn();
|
||||
const mockSetPageSize = jest.fn();
|
||||
|
||||
const mockProps: HostsListTableProps = {
|
||||
const mockProps = {
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
isFetching: false,
|
||||
@@ -73,7 +43,7 @@ describe('HostsListTable', () => {
|
||||
pageSize: 10,
|
||||
setOrderBy: mockSetOrderBy,
|
||||
setPageSize: mockSetPageSize,
|
||||
};
|
||||
} as any;
|
||||
|
||||
it('renders loading state if isLoading is true and tableData is empty', () => {
|
||||
const { container } = render(
|
||||
@@ -81,7 +51,7 @@ describe('HostsListTable', () => {
|
||||
{...mockProps}
|
||||
isLoading
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({ records: [] })}
|
||||
tableData={{ payload: { data: { hosts: [] } } }}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||
@@ -93,7 +63,7 @@ describe('HostsListTable', () => {
|
||||
{...mockProps}
|
||||
isFetching
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({ records: [] })}
|
||||
tableData={{ payload: { data: { hosts: [] } } }}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-loading-state')).toBeTruthy();
|
||||
@@ -104,56 +74,19 @@ describe('HostsListTable', () => {
|
||||
expect(screen.getByText('Something went wrong')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders "Something went wrong" fallback when isError is true and error message is empty', () => {
|
||||
const tableDataWithEmptyError: ErrorResponse = {
|
||||
statusCode: 500,
|
||||
payload: null,
|
||||
error: '',
|
||||
message: null,
|
||||
};
|
||||
render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
isError
|
||||
hostMetricsData={[]}
|
||||
tableData={tableDataWithEmptyError}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('Something went wrong')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders custom error message when isError is true and error message is provided', () => {
|
||||
const customErrorMessage = 'Failed to fetch host metrics';
|
||||
const tableDataWithError: ErrorResponse = {
|
||||
statusCode: 500,
|
||||
payload: null,
|
||||
error: customErrorMessage,
|
||||
message: null,
|
||||
};
|
||||
render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
isError
|
||||
hostMetricsData={[]}
|
||||
tableData={tableDataWithError}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText(customErrorMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders empty state if no hosts are found', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({
|
||||
records: [],
|
||||
})}
|
||||
tableData={{
|
||||
payload: {
|
||||
data: { hosts: [] },
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container.querySelector('.no-filtered-hosts-message-container'),
|
||||
).toBeTruthy();
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state if sentAnyHostMetricsData is false', () => {
|
||||
@@ -161,114 +94,58 @@ describe('HostsListTable', () => {
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({
|
||||
sentAnyHostMetricsData: false,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state if isSendingK8SAgentMetrics is true', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({
|
||||
isSendingK8SAgentMetrics: true,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders end time before retention message when endTimeBeforeRetention is true', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
endTimeBeforeRetention: true,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Your requested end time is earlier than the earliest detected time of host metrics data, please adjust your end time\./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no records message when noRecordsInSelectedTimeRangeAndFilters is true', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={createMockTableData({
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
container.querySelector('.no-filtered-hosts-message-container'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(/No host metrics in the selected time range and filters/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders no filtered hosts message when filters are present and no hosts are found', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
filters={{
|
||||
items: [
|
||||
{
|
||||
id: 'host_name',
|
||||
key: {
|
||||
key: 'host_name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isIndexed: true,
|
||||
},
|
||||
op: '=',
|
||||
value: 'unknown',
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
...mockTableData.payload,
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
sentAnyHostMetricsData: false,
|
||||
hosts: [],
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
}}
|
||||
tableData={createMockTableData({
|
||||
sentAnyHostMetricsData: true,
|
||||
isSendingK8SAgentMetrics: false,
|
||||
records: [],
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.no-filtered-hosts-message')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/No host metrics in the selected time range and filters\. Please adjust your time range or filters\./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders empty state if isSendingIncorrectK8SAgentMetrics is true', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
hostMetricsData={[]}
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
...mockTableData.payload,
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
isSendingIncorrectK8SAgentMetrics: true,
|
||||
hosts: [],
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector(EMPTY_STATE_CONTAINER_CLASS)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders table data', () => {
|
||||
const { container } = render(
|
||||
<HostsListTable
|
||||
{...mockProps}
|
||||
tableData={createMockTableData({
|
||||
isSendingK8SAgentMetrics: false,
|
||||
sentAnyHostMetricsData: true,
|
||||
})}
|
||||
tableData={{
|
||||
...mockTableData,
|
||||
payload: {
|
||||
...mockTableData.payload,
|
||||
data: {
|
||||
...mockTableData.payload.data,
|
||||
isSendingIncorrectK8SAgentMetrics: false,
|
||||
sentAnyHostMetricsData: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-table')).toBeTruthy();
|
||||
|
||||
@@ -107,17 +107,6 @@ export interface HostsListTableProps {
|
||||
setPageSize: (pageSize: number) => void;
|
||||
}
|
||||
|
||||
export interface EmptyOrLoadingViewProps {
|
||||
isError: boolean;
|
||||
errorMessage: string;
|
||||
showHostsEmptyState: boolean;
|
||||
sentAnyHostMetricsData: boolean;
|
||||
isSendingIncorrectK8SAgentMetrics: boolean;
|
||||
showEndTimeBeforeRetentionMessage: boolean;
|
||||
showNoRecordsInSelectedTimeRangeMessage: boolean;
|
||||
showTableLoadingState: boolean;
|
||||
}
|
||||
|
||||
export const getHostListsQuery = (): HostListPayload => ({
|
||||
filters: {
|
||||
items: [],
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
} from 'container/DashboardContainer/DashboardDescription/utils';
|
||||
import { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import useDashboardsListQueryParams from 'hooks/dashboard/useDashboardsListQueryParams';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
@@ -76,7 +77,6 @@ import {
|
||||
// see more: https://github.com/lucide-icons/lucide/issues/94
|
||||
import { handleContactSupport } from 'pages/Integrations/utils';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import {
|
||||
@@ -104,7 +104,7 @@ function DashboardsList(): JSX.Element {
|
||||
const {
|
||||
data: dashboardListResponse,
|
||||
isLoading: isDashboardListLoading,
|
||||
isRefetching: isDashboardListRefetching,
|
||||
isFetching: isDashboardListFetching,
|
||||
error: dashboardFetchError,
|
||||
refetch: refetchDashboardList,
|
||||
} = useGetAllDashboard();
|
||||
@@ -112,14 +112,14 @@ function DashboardsList(): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const {
|
||||
listSortOrder: sortOrder,
|
||||
setListSortOrder: setSortOrder,
|
||||
} = useDashboard();
|
||||
dashboardsListQueryParams,
|
||||
updateDashboardsListQueryParams,
|
||||
} = useDashboardsListQueryParams();
|
||||
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const [searchString, setSearchString] = useState<string>(
|
||||
sortOrder.search || '',
|
||||
dashboardsListQueryParams.search || '',
|
||||
);
|
||||
const [action, createNewDashboard] = useComponentPermission(
|
||||
['action', 'create_new_dashboards'],
|
||||
@@ -139,7 +139,6 @@ function DashboardsList(): JSX.Element {
|
||||
] = useState<boolean>(false);
|
||||
|
||||
const [uploadedGrafana, setUploadedGrafana] = useState<boolean>(false);
|
||||
const [isFilteringDashboards, setIsFilteringDashboards] = useState(false);
|
||||
const [isConfigureMetadataOpen, setIsConfigureMetadata] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
@@ -187,76 +186,41 @@ function DashboardsList(): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
const [dashboards, setDashboards] = useState<Dashboard[]>();
|
||||
|
||||
const sortDashboardsByCreatedAt = (dashboards: Dashboard[]): void => {
|
||||
const sortedDashboards = dashboards.sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
setDashboards(sortedDashboards);
|
||||
};
|
||||
|
||||
const sortDashboardsByUpdatedAt = (dashboards: Dashboard[]): void => {
|
||||
const sortedDashboards = dashboards.sort(
|
||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
||||
);
|
||||
setDashboards(sortedDashboards);
|
||||
};
|
||||
|
||||
const sortHandle = (key: string): void => {
|
||||
if (!dashboards) {
|
||||
return;
|
||||
}
|
||||
if (key === 'createdAt') {
|
||||
sortDashboardsByCreatedAt(dashboards);
|
||||
setSortOrder({
|
||||
columnKey: 'createdAt',
|
||||
order: 'descend',
|
||||
pagination: sortOrder.pagination || '1',
|
||||
search: sortOrder.search || '',
|
||||
});
|
||||
} else if (key === 'updatedAt') {
|
||||
sortDashboardsByUpdatedAt(dashboards);
|
||||
setSortOrder({
|
||||
columnKey: 'updatedAt',
|
||||
order: 'descend',
|
||||
pagination: sortOrder.pagination || '1',
|
||||
search: sortOrder.search || '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
function handlePageSizeUpdate(page: number): void {
|
||||
setSortOrder({ ...sortOrder, pagination: String(page) });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const filteredDashboards = filterDashboard(
|
||||
const dashboards = useMemo((): Dashboard[] => {
|
||||
const filtered = filterDashboard(
|
||||
searchString,
|
||||
dashboardListResponse?.data || [],
|
||||
);
|
||||
if (sortOrder.columnKey === 'updatedAt') {
|
||||
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
||||
} else if (sortOrder.columnKey === 'createdAt') {
|
||||
sortDashboardsByCreatedAt(filteredDashboards || []);
|
||||
} else if (sortOrder.columnKey === 'null') {
|
||||
setSortOrder({
|
||||
columnKey: 'updatedAt',
|
||||
order: 'descend',
|
||||
pagination: sortOrder.pagination || '1',
|
||||
search: sortOrder.search || '',
|
||||
});
|
||||
sortDashboardsByUpdatedAt(filteredDashboards || []);
|
||||
if (dashboardsListQueryParams.columnKey === 'createdAt') {
|
||||
return filtered.sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
}
|
||||
return filtered.sort(
|
||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(),
|
||||
);
|
||||
}, [
|
||||
dashboardListResponse,
|
||||
dashboardListResponse?.data,
|
||||
searchString,
|
||||
setSortOrder,
|
||||
sortOrder.columnKey,
|
||||
sortOrder.pagination,
|
||||
sortOrder.search,
|
||||
dashboardsListQueryParams.columnKey,
|
||||
]);
|
||||
|
||||
const sortHandle = (key: string): void => {
|
||||
updateDashboardsListQueryParams({
|
||||
columnKey: key,
|
||||
order: 'descend',
|
||||
page: dashboardsListQueryParams.page || '1',
|
||||
search: dashboardsListQueryParams.search || '',
|
||||
});
|
||||
};
|
||||
|
||||
function handlePageSizeUpdate(page: number): void {
|
||||
updateDashboardsListQueryParams({
|
||||
...dashboardsListQueryParams,
|
||||
page: String(page),
|
||||
});
|
||||
}
|
||||
|
||||
const [newDashboardState, setNewDashboardState] = useState({
|
||||
loading: false,
|
||||
error: false,
|
||||
@@ -265,26 +229,25 @@ function DashboardsList(): JSX.Element {
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
const data: Data[] =
|
||||
dashboards?.map((e) => ({
|
||||
createdAt: e.createdAt,
|
||||
description: e.data.description || '',
|
||||
id: e.id,
|
||||
lastUpdatedTime: e.updatedAt,
|
||||
name: e.data.title,
|
||||
tags: e.data.tags || [],
|
||||
key: e.id,
|
||||
createdBy: e.createdBy,
|
||||
isLocked: !!e.locked || false,
|
||||
lastUpdatedBy: e.updatedBy,
|
||||
image: e.data.image || Base64Icons[0],
|
||||
variables: e.data.variables,
|
||||
widgets: e.data.widgets,
|
||||
layout: e.data.layout,
|
||||
panelMap: e.data.panelMap,
|
||||
version: e.data.version,
|
||||
refetchDashboardList,
|
||||
})) || [];
|
||||
const data: Data[] = dashboards.map((e) => ({
|
||||
createdAt: e.createdAt,
|
||||
description: e.data.description || '',
|
||||
id: e.id,
|
||||
lastUpdatedTime: e.updatedAt,
|
||||
name: e.data.title,
|
||||
tags: e.data.tags || [],
|
||||
key: e.id,
|
||||
createdBy: e.createdBy,
|
||||
isLocked: !!e.locked || false,
|
||||
lastUpdatedBy: e.updatedBy,
|
||||
image: e.data.image || Base64Icons[0],
|
||||
variables: e.data.variables,
|
||||
widgets: e.data.widgets,
|
||||
layout: e.data.layout,
|
||||
panelMap: e.data.panelMap,
|
||||
version: e.data.version,
|
||||
refetchDashboardList,
|
||||
}));
|
||||
|
||||
const onNewDashboardHandler = useCallback(async () => {
|
||||
try {
|
||||
@@ -324,16 +287,12 @@ function DashboardsList(): JSX.Element {
|
||||
};
|
||||
|
||||
const handleSearch = (event: ChangeEvent<HTMLInputElement>): void => {
|
||||
setIsFilteringDashboards(true);
|
||||
const searchText = (event as React.BaseSyntheticEvent)?.target?.value || '';
|
||||
const filteredDashboards = filterDashboard(
|
||||
searchText,
|
||||
dashboardListResponse?.data || [],
|
||||
);
|
||||
setDashboards(filteredDashboards);
|
||||
setIsFilteringDashboards(false);
|
||||
setSearchString(searchText);
|
||||
setSortOrder({ ...sortOrder, search: searchText });
|
||||
updateDashboardsListQueryParams({
|
||||
...dashboardsListQueryParams,
|
||||
search: searchText,
|
||||
});
|
||||
};
|
||||
|
||||
const [state, setCopy] = useCopyToClipboard();
|
||||
@@ -671,8 +630,8 @@ function DashboardsList(): JSX.Element {
|
||||
showTotal: showPaginationItem,
|
||||
showSizeChanger: false,
|
||||
onChange: (page: any): void => handlePageSizeUpdate(page),
|
||||
current: Number(sortOrder.pagination),
|
||||
defaultCurrent: Number(sortOrder.pagination) || 1,
|
||||
current: Number(dashboardsListQueryParams.page),
|
||||
defaultCurrent: Number(dashboardsListQueryParams.page) || 1,
|
||||
hideOnSinglePage: true,
|
||||
};
|
||||
|
||||
@@ -710,9 +669,7 @@ function DashboardsList(): JSX.Element {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isDashboardListLoading ||
|
||||
isFilteringDashboards ||
|
||||
isDashboardListRefetching ? (
|
||||
{isDashboardListFetching ? (
|
||||
<div className="loading-dashboard-details">
|
||||
<Skeleton.Input active size="large" className="skeleton-1" />
|
||||
<Skeleton.Input active size="large" className="skeleton-1" />
|
||||
@@ -749,7 +706,7 @@ function DashboardsList(): JSX.Element {
|
||||
<ArrowUpRight size={16} className="learn-more-arrow" />
|
||||
</section>
|
||||
</div>
|
||||
) : dashboards?.length === 0 && !searchString ? (
|
||||
) : dashboards.length === 0 && !searchString ? (
|
||||
<div className="dashboard-empty-state">
|
||||
<img
|
||||
src="/Icons/dashboards.svg"
|
||||
@@ -831,7 +788,7 @@ function DashboardsList(): JSX.Element {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{dashboards?.length === 0 ? (
|
||||
{dashboards.length === 0 ? (
|
||||
<div className="no-search">
|
||||
<img src="/Icons/emptyState.svg" alt="img" className="img" />
|
||||
<Typography.Text className="text">
|
||||
@@ -860,7 +817,9 @@ function DashboardsList(): JSX.Element {
|
||||
data-testid="sort-by-last-created"
|
||||
>
|
||||
Last created
|
||||
{sortOrder.columnKey === 'createdAt' && <Check size={14} />}
|
||||
{dashboardsListQueryParams.columnKey === 'createdAt' && (
|
||||
<Check size={14} />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
@@ -869,7 +828,9 @@ function DashboardsList(): JSX.Element {
|
||||
data-testid="sort-by-last-updated"
|
||||
>
|
||||
Last updated
|
||||
{sortOrder.columnKey === 'updatedAt' && <Check size={14} />}
|
||||
{dashboardsListQueryParams.columnKey === 'updatedAt' && (
|
||||
<Check size={14} />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
@@ -911,11 +872,7 @@ function DashboardsList(): JSX.Element {
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
showSorterTooltip
|
||||
loading={
|
||||
isDashboardListLoading ||
|
||||
isFilteringDashboards ||
|
||||
isDashboardListRefetching
|
||||
}
|
||||
loading={isDashboardListFetching}
|
||||
showHeader={false}
|
||||
pagination={paginationConfig}
|
||||
/>
|
||||
@@ -964,12 +921,12 @@ function DashboardsList(): JSX.Element {
|
||||
<div className="configure-preview">
|
||||
<section className="header">
|
||||
<img
|
||||
src={dashboards?.[0]?.data?.image || Base64Icons[0]}
|
||||
src={dashboards[0]?.data?.image || Base64Icons[0]}
|
||||
alt="dashboard-image"
|
||||
style={{ height: '14px', width: '14px' }}
|
||||
/>
|
||||
<Typography.Text className="title">
|
||||
{dashboards?.[0]?.data?.title}
|
||||
{dashboards[0]?.data?.title}
|
||||
</Typography.Text>
|
||||
</section>
|
||||
<section className="details">
|
||||
@@ -977,16 +934,16 @@ function DashboardsList(): JSX.Element {
|
||||
{visibleColumns.createdAt && (
|
||||
<Typography.Text className="formatted-time">
|
||||
<CalendarClock size={14} />
|
||||
{getFormattedTime(dashboards?.[0] as Dashboard, 'created_at')}
|
||||
{getFormattedTime(dashboards[0] as Dashboard, 'created_at')}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{visibleColumns.createdBy && (
|
||||
<div className="user">
|
||||
<Typography.Text className="user-tag">
|
||||
{dashboards?.[0]?.createdBy?.substring(0, 1).toUpperCase()}
|
||||
{dashboards[0]?.createdBy?.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="dashboard-created-by">
|
||||
{dashboards?.[0]?.createdBy}
|
||||
{dashboards[0]?.createdBy}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
@@ -995,16 +952,16 @@ function DashboardsList(): JSX.Element {
|
||||
{visibleColumns.updatedAt && (
|
||||
<Typography.Text className="formatted-time">
|
||||
<CalendarClock size={14} />
|
||||
{onLastUpdated(dashboards?.[0]?.updatedAt || '')}
|
||||
{onLastUpdated(dashboards[0]?.updatedAt || '')}
|
||||
</Typography.Text>
|
||||
)}
|
||||
{visibleColumns.updatedBy && (
|
||||
<div className="user">
|
||||
<Typography.Text className="user-tag">
|
||||
{dashboards?.[0]?.updatedBy?.substring(0, 1).toUpperCase()}
|
||||
{dashboards[0]?.updatedBy?.substring(0, 1).toUpperCase()}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="dashboard-created-by">
|
||||
{dashboards?.[0]?.updatedBy}
|
||||
{dashboards[0]?.updatedBy}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -121,23 +121,9 @@ function BodyTitleRenderer({
|
||||
return (
|
||||
<TitleWrapper onClick={handleNodeClick}>
|
||||
{typeof value !== 'object' && (
|
||||
<span
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
onMouseDown={(e): void => e.preventDefault()}
|
||||
>
|
||||
<Dropdown
|
||||
menu={menu}
|
||||
trigger={['click']}
|
||||
dropdownRender={(originNode): React.ReactNode => (
|
||||
<div data-log-detail-ignore="true">{originNode}</div>
|
||||
)}
|
||||
>
|
||||
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
|
||||
</Dropdown>
|
||||
</span>
|
||||
<Dropdown menu={menu} trigger={['click']}>
|
||||
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
|
||||
</Dropdown>
|
||||
)}
|
||||
{title.toString()}{' '}
|
||||
{!parentIsArray && typeof value !== 'object' && (
|
||||
|
||||
@@ -13,7 +13,7 @@ function FieldRenderer({ field }: FieldRendererProps): JSX.Element {
|
||||
<span className="field-renderer-container">
|
||||
{dataType && newField && logType ? (
|
||||
<>
|
||||
<Tooltip placement="left" title={newField} mouseLeaveDelay={0}>
|
||||
<Tooltip placement="left" title={newField}>
|
||||
<Typography.Text ellipsis className="label">
|
||||
{newField}{' '}
|
||||
</Typography.Text>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import type { InputRef } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
@@ -52,6 +53,17 @@ function Overview({
|
||||
);
|
||||
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
|
||||
|
||||
const focusTimerRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const searchInputRef = useCallback((node: InputRef | null) => {
|
||||
clearTimeout(focusTimerRef.current);
|
||||
if (node) {
|
||||
focusTimerRef.current = setTimeout(() => node.focus(), 100);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => (): void => clearTimeout(focusTimerRef.current), []);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const options: EditorProps['options'] = {
|
||||
@@ -196,7 +208,7 @@ function Overview({
|
||||
<>
|
||||
{isSearchVisible && (
|
||||
<Input
|
||||
autoFocus
|
||||
ref={searchInputRef}
|
||||
placeholder="Search for a field..."
|
||||
className="search-input"
|
||||
value={fieldSearchInput}
|
||||
|
||||
@@ -245,7 +245,7 @@ function TableView({
|
||||
<Typography.Text>{renderedField}</Typography.Text>
|
||||
|
||||
{traceId && (
|
||||
<Tooltip title="Inspect in Trace" mouseLeaveDelay={0}>
|
||||
<Tooltip title="Inspect in Trace">
|
||||
<Button
|
||||
className="periscope-btn"
|
||||
onClick={(
|
||||
|
||||
@@ -139,7 +139,8 @@ const InfinityTable = forwardRef<TableVirtuosoHandle, InfinityTableProps>(
|
||||
{tableColumns
|
||||
.filter((column) => column.key)
|
||||
.map((column) => {
|
||||
const isDragColumn = column.key !== 'expand';
|
||||
const isDragColumn =
|
||||
column.key !== 'expand' && column.key !== 'state-indicator';
|
||||
|
||||
return (
|
||||
<TableHeaderCellStyled
|
||||
|
||||
@@ -87,7 +87,7 @@ function Explorer(): JSX.Element {
|
||||
const [yAxisUnit, setYAxisUnit] = useState<string | undefined>();
|
||||
|
||||
const unitsLength = useMemo(() => units.length, [units]);
|
||||
const firstUnit = useMemo(() => units[0], [units]);
|
||||
const firstUnit = useMemo(() => units?.[0], [units]);
|
||||
|
||||
useEffect(() => {
|
||||
// Set the y axis unit to the first metric unit if
|
||||
@@ -349,7 +349,7 @@ function Explorer(): JSX.Element {
|
||||
isOneChartPerQuery={showOneChartPerQuery}
|
||||
splitedQueries={splitedQueries}
|
||||
/>
|
||||
{isMetricDetailsOpen && selectedMetricName && (
|
||||
{isMetricDetailsOpen && (
|
||||
<MetricDetails
|
||||
metricName={selectedMetricName}
|
||||
isOpen={isMetricDetailsOpen}
|
||||
|
||||
@@ -39,7 +39,10 @@ function RelatedMetricsCard({ metric }: RelatedMetricsCardProps): JSX.Element {
|
||||
dataSource={DataSource.METRICS}
|
||||
/>
|
||||
)}
|
||||
<DashboardsAndAlertsPopover metricName={metric.name} />
|
||||
<DashboardsAndAlertsPopover
|
||||
dashboards={metric.dashboards}
|
||||
alerts={metric.alerts}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQueries, useQueryClient } from 'react-query';
|
||||
import { useQueries } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import {
|
||||
invalidateGetMetricMetadata,
|
||||
useUpdateMetricMetadata,
|
||||
} from 'api/generated/services/metrics';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import { isAxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||
@@ -28,10 +23,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { TimeSeriesProps } from './types';
|
||||
import {
|
||||
buildUpdateMetricYAxisUnitPayload,
|
||||
splitQueryIntoOneChartPerQuery,
|
||||
} from './utils';
|
||||
import { splitQueryIntoOneChartPerQuery } from './utils';
|
||||
|
||||
function TimeSeries({
|
||||
showOneChartPerQuery,
|
||||
@@ -43,7 +35,6 @@ function TimeSeries({
|
||||
yAxisUnit,
|
||||
setYAxisUnit,
|
||||
showYAxisUnitSelector,
|
||||
metrics,
|
||||
}: TimeSeriesProps): JSX.Element {
|
||||
const { stagedQuery, currentQuery } = useQueryBuilder();
|
||||
|
||||
@@ -51,7 +42,6 @@ function TimeSeries({
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const isValidToConvertToMs = useMemo(() => {
|
||||
const isValid: boolean[] = [];
|
||||
@@ -148,51 +138,54 @@ function TimeSeries({
|
||||
setYAxisUnit(value);
|
||||
};
|
||||
|
||||
// TODO: Enable once we have resolved all related metrics v2 api issues
|
||||
// Show the save unit button if
|
||||
// 1. There is only one metric
|
||||
// 2. The metric has no saved unit
|
||||
// 3. The user has selected a unit
|
||||
const showSaveUnitButton = useMemo(
|
||||
() =>
|
||||
metricUnits.length === 1 &&
|
||||
Boolean(metrics[0]) &&
|
||||
!metricUnits[0] &&
|
||||
yAxisUnit,
|
||||
[metricUnits, metrics, yAxisUnit],
|
||||
);
|
||||
// const showSaveUnitButton = useMemo(
|
||||
// () =>
|
||||
// metricUnits.length === 1 &&
|
||||
// Boolean(metrics?.[0]) &&
|
||||
// !metricUnits[0] &&
|
||||
// yAxisUnit,
|
||||
// [metricUnits, metrics, yAxisUnit],
|
||||
// );
|
||||
|
||||
const {
|
||||
mutate: updateMetricMetadata,
|
||||
isLoading: isUpdatingMetricMetadata,
|
||||
} = useUpdateMetricMetadata();
|
||||
// const {
|
||||
// mutate: updateMetricMetadata,
|
||||
// isLoading: isUpdatingMetricMetadata,
|
||||
// } = useUpdateMetricMetadata();
|
||||
|
||||
const handleSaveUnit = (): void => {
|
||||
if (metrics[0] && yAxisUnit) {
|
||||
updateMetricMetadata(
|
||||
{
|
||||
pathParams: {
|
||||
metricName: metricNames[0],
|
||||
},
|
||||
data: buildUpdateMetricYAxisUnitPayload(
|
||||
metricNames[0],
|
||||
metrics[0],
|
||||
yAxisUnit,
|
||||
),
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success('Unit saved successfully');
|
||||
invalidateGetMetricMetadata(queryClient, {
|
||||
metricName: metricNames[0],
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast.error('Failed to save unit');
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
// const handleSaveUnit = (): void => {
|
||||
// updateMetricMetadata(
|
||||
// {
|
||||
// metricName: metricNames[0],
|
||||
// payload: {
|
||||
// unit: yAxisUnit,
|
||||
// description: metrics[0]?.description ?? '',
|
||||
// metricType: metrics[0]?.type as MetricType,
|
||||
// temporality: metrics[0]?.temporality,
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// onSuccess: () => {
|
||||
// notifications.success({
|
||||
// message: 'Unit saved successfully',
|
||||
// });
|
||||
// queryClient.invalidateQueries([
|
||||
// REACT_QUERY_KEY.GET_METRIC_DETAILS,
|
||||
// metricNames[0],
|
||||
// ]);
|
||||
// },
|
||||
// onError: () => {
|
||||
// notifications.error({
|
||||
// message: 'Failed to save unit',
|
||||
// });
|
||||
// },
|
||||
// },
|
||||
// );
|
||||
// };
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -205,7 +198,8 @@ function TimeSeries({
|
||||
source={YAxisSource.EXPLORER}
|
||||
data-testid="y-axis-unit-selector"
|
||||
/>
|
||||
{showSaveUnitButton && (
|
||||
{/* TODO: Enable once we have resolved all related metrics v2 api issues */}
|
||||
{/* {showSaveUnitButton && (
|
||||
<div className="save-unit-container">
|
||||
<Typography.Text>
|
||||
Save the selected unit for this metric?
|
||||
@@ -219,7 +213,7 @@ function TimeSeries({
|
||||
<Typography.Paragraph>Yes</Typography.Paragraph>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,8 @@ import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import {
|
||||
MetrictypesTemporalityDTO,
|
||||
MetrictypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import * as useOptionsMenuHooks from 'container/OptionsMenu';
|
||||
import * as useUpdateDashboardHooks from 'hooks/dashboard/useUpdateDashboard';
|
||||
@@ -16,12 +14,12 @@ import { ErrorModalProvider } from 'providers/ErrorModalProvider';
|
||||
import * as timezoneHooks from 'providers/Timezone';
|
||||
import store from 'store';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||
|
||||
import Explorer from '../Explorer';
|
||||
import * as useGetMetricsHooks from '../utils';
|
||||
import { MOCK_METRIC_METADATA } from './testUtils';
|
||||
|
||||
const mockSetSearchParams = jest.fn();
|
||||
const queryClient = new QueryClient();
|
||||
@@ -137,6 +135,14 @@ jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
|
||||
|
||||
const Y_AXIS_UNIT_SELECTOR_TEST_ID = 'y-axis-unit-selector';
|
||||
|
||||
const mockMetric: MetricMetadata = {
|
||||
type: MetricType.SUM,
|
||||
description: 'metric1 description',
|
||||
unit: 'metric1 unit',
|
||||
temporality: Temporality.CUMULATIVE,
|
||||
isMonotonic: true,
|
||||
};
|
||||
|
||||
function renderExplorer(): void {
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
@@ -184,7 +190,7 @@ describe('Explorer', () => {
|
||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
||||
metrics: [mockMetric, mockMetric],
|
||||
});
|
||||
|
||||
renderExplorer();
|
||||
@@ -201,7 +207,7 @@ describe('Explorer', () => {
|
||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
||||
metrics: [mockMetric, mockMetric],
|
||||
});
|
||||
|
||||
renderExplorer();
|
||||
@@ -214,7 +220,7 @@ describe('Explorer', () => {
|
||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [MOCK_METRIC_METADATA],
|
||||
metrics: [mockMetric],
|
||||
});
|
||||
|
||||
renderExplorer();
|
||||
@@ -231,7 +237,7 @@ describe('Explorer', () => {
|
||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
||||
metrics: [mockMetric, mockMetric],
|
||||
});
|
||||
|
||||
renderExplorer();
|
||||
@@ -244,7 +250,7 @@ describe('Explorer', () => {
|
||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
||||
metrics: [mockMetric, mockMetric],
|
||||
});
|
||||
|
||||
renderExplorer();
|
||||
@@ -263,10 +269,10 @@ describe('Explorer', () => {
|
||||
isError: false,
|
||||
metrics: [
|
||||
{
|
||||
type: MetrictypesTypeDTO.sum,
|
||||
type: MetricType.SUM,
|
||||
description: 'metric1 description',
|
||||
unit: '',
|
||||
temporality: MetrictypesTemporalityDTO.cumulative,
|
||||
temporality: Temporality.CUMULATIVE,
|
||||
isMonotonic: true,
|
||||
},
|
||||
],
|
||||
@@ -283,7 +289,7 @@ describe('Explorer', () => {
|
||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [MOCK_METRIC_METADATA],
|
||||
metrics: [mockMetric],
|
||||
});
|
||||
|
||||
renderExplorer();
|
||||
@@ -318,7 +324,7 @@ describe('Explorer', () => {
|
||||
jest.spyOn(useGetMetricsHooks, 'useGetMetrics').mockReturnValue({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
||||
metrics: [mockMetric, mockMetric],
|
||||
});
|
||||
|
||||
renderExplorer();
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import { UseMutationResult } from 'react-query';
|
||||
import { render, RenderResult, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import { UpdateMetricMetadataResponse } from 'api/metricsExplorer/updateMetricMetadata';
|
||||
import * as useUpdateMetricMetadataHooks from 'hooks/metricsExplorer/useUpdateMetricMetadata';
|
||||
import { UseUpdateMetricMetadataProps } from 'hooks/metricsExplorer/useUpdateMetricMetadata';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
||||
|
||||
import TimeSeries from '../TimeSeries';
|
||||
import { TimeSeriesProps } from '../types';
|
||||
import { MOCK_METRIC_METADATA } from './testUtils';
|
||||
|
||||
const mockUpdateMetricMetadata = jest.fn();
|
||||
const updateMetricMetadataSpy = jest.spyOn(
|
||||
metricsExplorerHooks,
|
||||
'useUpdateMetricMetadata',
|
||||
);
|
||||
type UseUpdateMetricMetadataReturnType = ReturnType<
|
||||
typeof metricsExplorerHooks.useUpdateMetricMetadata
|
||||
type MockUpdateMetricMetadata = UseMutationResult<
|
||||
SuccessResponse<UpdateMetricMetadataResponse> | ErrorResponse,
|
||||
Error,
|
||||
UseUpdateMetricMetadataProps
|
||||
>;
|
||||
const mockUpdateMetricMetadata = jest.fn();
|
||||
jest
|
||||
.spyOn(useUpdateMetricMetadataHooks, 'useUpdateMetricMetadata')
|
||||
.mockReturnValue(({
|
||||
mutate: mockUpdateMetricMetadata,
|
||||
isLoading: false,
|
||||
} as Partial<MockUpdateMetricMetadata>) as MockUpdateMetricMetadata);
|
||||
|
||||
jest.mock('container/TimeSeriesView/TimeSeriesView', () => ({
|
||||
__esModule: true,
|
||||
@@ -50,6 +60,14 @@ jest.mock('react-redux', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockMetric: MetricMetadata = {
|
||||
type: MetricType.SUM,
|
||||
description: 'metric1 description',
|
||||
unit: 'metric1 unit',
|
||||
temporality: Temporality.CUMULATIVE,
|
||||
isMonotonic: true,
|
||||
};
|
||||
|
||||
const mockSetWarning = jest.fn();
|
||||
const mockSetIsMetricDetailsOpen = jest.fn();
|
||||
const mockSetYAxisUnit = jest.fn();
|
||||
@@ -78,13 +96,6 @@ function renderTimeSeries(
|
||||
}
|
||||
|
||||
describe('TimeSeries', () => {
|
||||
beforeEach(() => {
|
||||
updateMetricMetadataSpy.mockReturnValue(({
|
||||
mutate: mockUpdateMetricMetadata,
|
||||
isLoading: false,
|
||||
} as Partial<UseUpdateMetricMetadataReturnType>) as UseUpdateMetricMetadataReturnType);
|
||||
});
|
||||
|
||||
it('should render a warning icon when a metric has no unit among multiple metrics', () => {
|
||||
const user = userEvent.setup();
|
||||
const { container } = renderTimeSeries({
|
||||
@@ -107,7 +118,7 @@ describe('TimeSeries', () => {
|
||||
const { container } = renderTimeSeries({
|
||||
metricUnits: ['', 'count'],
|
||||
metricNames: ['metric1', 'metric2'],
|
||||
metrics: [MOCK_METRIC_METADATA, MOCK_METRIC_METADATA],
|
||||
metrics: [mockMetric, mockMetric],
|
||||
yAxisUnit: 'seconds',
|
||||
});
|
||||
|
||||
@@ -122,17 +133,18 @@ describe('TimeSeries', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('shows Save unit button when metric had no unit but one is selected', async () => {
|
||||
// TODO: Unskip this test once the save unit button is implemented
|
||||
// Tracking at - https://github.com/SigNoz/engineering-pod/issues/3495
|
||||
it.skip('shows Save unit button when metric had no unit but one is selected', () => {
|
||||
const { findByText, getByRole } = renderTimeSeries({
|
||||
metricUnits: [undefined],
|
||||
metricNames: ['metric1'],
|
||||
metrics: [MOCK_METRIC_METADATA],
|
||||
metrics: [mockMetric],
|
||||
yAxisUnit: 'seconds',
|
||||
showYAxisUnitSelector: true,
|
||||
});
|
||||
|
||||
expect(
|
||||
await findByText('Save the selected unit for this metric?'),
|
||||
findByText('Save the selected unit for this metric?'),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const yesButton = getByRole('button', { name: 'Yes' });
|
||||
@@ -140,25 +152,24 @@ describe('TimeSeries', () => {
|
||||
expect(yesButton).toBeEnabled();
|
||||
});
|
||||
|
||||
it('clicking on save unit button shoould upated metric metadata', async () => {
|
||||
// TODO: Unskip this test once the save unit button is implemented
|
||||
// Tracking at - https://github.com/SigNoz/engineering-pod/issues/3495
|
||||
it.skip('clicking on save unit button shoould upated metric metadata', () => {
|
||||
const user = userEvent.setup();
|
||||
const { getByRole } = renderTimeSeries({
|
||||
metricUnits: [''],
|
||||
metricNames: ['metric1'],
|
||||
metrics: [MOCK_METRIC_METADATA],
|
||||
metrics: [mockMetric],
|
||||
yAxisUnit: 'seconds',
|
||||
showYAxisUnitSelector: true,
|
||||
});
|
||||
|
||||
const yesButton = getByRole('button', { name: /Yes/i });
|
||||
await user.click(yesButton);
|
||||
user.click(yesButton);
|
||||
|
||||
expect(mockUpdateMetricMetadata).toHaveBeenCalledWith(
|
||||
{
|
||||
pathParams: {
|
||||
metricName: 'metric1',
|
||||
},
|
||||
data: expect.objectContaining({ unit: 'seconds' }),
|
||||
metricName: 'metric1',
|
||||
payload: expect.objectContaining({ unit: 'seconds' }),
|
||||
},
|
||||
expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import {
|
||||
MetricsexplorertypesMetricMetadataDTO,
|
||||
MetrictypesTemporalityDTO,
|
||||
MetrictypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export const MOCK_METRIC_METADATA: MetricsexplorertypesMetricMetadataDTO = {
|
||||
type: MetrictypesTypeDTO.sum,
|
||||
description: 'metric1 description',
|
||||
unit: 'metric1 unit',
|
||||
temporality: MetrictypesTemporalityDTO.cumulative,
|
||||
isMonotonic: true,
|
||||
};
|
||||
@@ -1,8 +1,14 @@
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { GetMetricMetadata200 } from 'api/generated/services/sigNoz.schemas';
|
||||
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import * as useGetMultipleMetricsHook from 'hooks/metricsExplorer/useGetMultipleMetrics';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import {
|
||||
MetricMetadata,
|
||||
MetricMetadataResponse,
|
||||
} from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderFormula,
|
||||
@@ -16,7 +22,6 @@ import {
|
||||
splitQueryIntoOneChartPerQuery,
|
||||
useGetMetrics,
|
||||
} from '../utils';
|
||||
import { MOCK_METRIC_METADATA } from './testUtils';
|
||||
|
||||
const MOCK_QUERY_DATA_1: IBuilderQuery = {
|
||||
...initialQueriesMap[DataSource.METRICS].builder.queryData[0],
|
||||
@@ -86,19 +91,32 @@ describe('splitQueryIntoOneChartPerQuery', () => {
|
||||
});
|
||||
});
|
||||
|
||||
const MOCK_METRIC_METADATA: MetricMetadata = {
|
||||
description: 'Metric 1 description',
|
||||
unit: 'unit1',
|
||||
type: MetricType.GAUGE,
|
||||
temporality: Temporality.DELTA,
|
||||
isMonotonic: true,
|
||||
};
|
||||
|
||||
describe('useGetMetrics', () => {
|
||||
beforeEach(() => {
|
||||
jest
|
||||
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
|
||||
.mockReturnValue([
|
||||
{
|
||||
({
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
data: {
|
||||
data: MOCK_METRIC_METADATA,
|
||||
status: 'success',
|
||||
httpStatusCode: 200,
|
||||
data: {
|
||||
status: 'success',
|
||||
data: MOCK_METRIC_METADATA,
|
||||
},
|
||||
},
|
||||
} as UseQueryResult<GetMetricMetadata200, Error>,
|
||||
} as Partial<
|
||||
UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>
|
||||
>) as UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -115,11 +133,12 @@ describe('useGetMetrics', () => {
|
||||
jest
|
||||
.spyOn(useGetMultipleMetricsHook, 'useGetMultipleMetrics')
|
||||
.mockReturnValue([
|
||||
{
|
||||
({
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
data: undefined,
|
||||
} as UseQueryResult<GetMetricMetadata200, Error>,
|
||||
} as Partial<
|
||||
UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>
|
||||
>) as UseQueryResult<SuccessResponseV2<MetricMetadataResponse>, Error>,
|
||||
]);
|
||||
const { result } = renderHook(() => useGetMetrics(['metric1']));
|
||||
expect(result.current.metrics).toHaveLength(1);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics';
|
||||
import { SuccessResponse, Warning } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
||||
|
||||
export enum ExplorerTabs {
|
||||
TIME_SERIES = 'time-series',
|
||||
@@ -18,7 +18,7 @@ export interface TimeSeriesProps {
|
||||
isMetricUnitsError: boolean;
|
||||
metricUnits: (string | undefined)[];
|
||||
metricNames: string[];
|
||||
metrics: (MetricsexplorertypesMetricMetadataDTO | undefined)[];
|
||||
metrics: (MetricMetadata | undefined)[];
|
||||
handleOpenMetricDetails: (metricName: string) => void;
|
||||
yAxisUnit: string | undefined;
|
||||
setYAxisUnit: (unit: string) => void;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { UpdateMetricMetadataMutationBody } from 'api/generated/services/metrics';
|
||||
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { mapMetricUnitToUniversalUnit } from 'components/YAxisUnitSelector/utils';
|
||||
import { useGetMultipleMetrics } from 'hooks/metricsExplorer/useGetMultipleMetrics';
|
||||
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { determineIsMonotonic } from '../MetricDetails/utils';
|
||||
|
||||
/**
|
||||
* Split a query with multiple queryData to multiple distinct queries, each with a single queryData.
|
||||
* @param query - The query to split
|
||||
@@ -71,14 +68,16 @@ export function useGetMetrics(
|
||||
): {
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
metrics: (MetricsexplorertypesMetricMetadataDTO | undefined)[];
|
||||
metrics: (MetricMetadata | undefined)[];
|
||||
} {
|
||||
const metricsData = useGetMultipleMetrics(metricNames, {
|
||||
enabled: metricNames.length > 0 && isEnabled,
|
||||
});
|
||||
return {
|
||||
isLoading: metricsData.some((metric) => metric.isLoading),
|
||||
metrics: metricsData.map((metric) => metric.data?.data),
|
||||
metrics: metricsData
|
||||
.map((metric) => metric.data?.data)
|
||||
.map((data) => data?.data),
|
||||
isError: metricsData.some((metric) => metric.isError),
|
||||
};
|
||||
}
|
||||
@@ -90,24 +89,9 @@ export function useGetMetrics(
|
||||
* @returns The units of the metrics, can be undefined if the metric has no unit
|
||||
*/
|
||||
export function getMetricUnits(
|
||||
metrics: (MetricsexplorertypesMetricMetadataDTO | undefined)[],
|
||||
metrics: (MetricMetadata | undefined)[],
|
||||
): (string | undefined)[] {
|
||||
return metrics
|
||||
.map((metric) => metric?.unit)
|
||||
.map((unit) => mapMetricUnitToUniversalUnit(unit) || undefined);
|
||||
}
|
||||
|
||||
export function buildUpdateMetricYAxisUnitPayload(
|
||||
metricName: string,
|
||||
metric: MetricsexplorertypesMetricMetadataDTO,
|
||||
yAxisUnit: string,
|
||||
): UpdateMetricMetadataMutationBody {
|
||||
return {
|
||||
metricName,
|
||||
type: metric.type,
|
||||
description: metric.description,
|
||||
unit: yAxisUnit || '',
|
||||
temporality: metric.temporality,
|
||||
isMonotonic: determineIsMonotonic(metric?.type, metric?.temporality),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Card, Input, Select, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { InspectMetricsSeries } from 'api/metricsExplorer/getInspectMetricsDetails';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import classNames from 'classnames';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { AggregatorFilter } from 'container/QueryBuilder/filters';
|
||||
@@ -40,10 +40,8 @@ import {
|
||||
* returns true if the feature flag is enabled, false otherwise
|
||||
* Show the inspect button in metrics explorer if the feature flag is enabled
|
||||
*/
|
||||
export function isInspectEnabled(
|
||||
metricType: MetrictypesTypeDTO | undefined,
|
||||
): boolean {
|
||||
return metricType === MetrictypesTypeDTO.gauge;
|
||||
export function isInspectEnabled(metricType: MetricType | undefined): boolean {
|
||||
return metricType === MetricType.GAUGE;
|
||||
}
|
||||
|
||||
export function getAllTimestampsOfMetrics(
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import {
|
||||
Button,
|
||||
Collapse,
|
||||
Input,
|
||||
Menu,
|
||||
Popover,
|
||||
Skeleton,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { Button, Collapse, Input, Menu, Popover, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useGetMetricAttributes } from 'api/generated/services/metrics';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { DataType } from 'container/LogDetailedView/TableView';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
@@ -21,33 +12,9 @@ import { PANEL_TYPES } from '../../../constants/queryBuilder';
|
||||
import ROUTES from '../../../constants/routes';
|
||||
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import MetricDetailsErrorState from './MetricDetailsErrorState';
|
||||
import {
|
||||
AllAttributesEmptyTextProps,
|
||||
AllAttributesProps,
|
||||
AllAttributesValueProps,
|
||||
} from './types';
|
||||
import { AllAttributesProps, AllAttributesValueProps } from './types';
|
||||
import { getMetricDetailsQuery } from './utils';
|
||||
|
||||
const ALL_ATTRIBUTES_KEY = 'all-attributes';
|
||||
|
||||
function AllAttributesEmptyText({
|
||||
isErrorAttributes,
|
||||
refetchAttributes,
|
||||
}: AllAttributesEmptyTextProps): JSX.Element {
|
||||
if (isErrorAttributes) {
|
||||
return (
|
||||
<div className="all-attributes-error-state">
|
||||
<MetricDetailsErrorState
|
||||
refetch={refetchAttributes}
|
||||
errorMessage="Something went wrong while fetching attributes"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <Typography.Text>No attributes found</Typography.Text>;
|
||||
}
|
||||
|
||||
export function AllAttributesValue({
|
||||
filterKey,
|
||||
filterValue,
|
||||
@@ -143,23 +110,13 @@ export function AllAttributesValue({
|
||||
|
||||
function AllAttributes({
|
||||
metricName,
|
||||
attributes,
|
||||
metricType,
|
||||
}: AllAttributesProps): JSX.Element {
|
||||
const [searchString, setSearchString] = useState('');
|
||||
const [activeKey, setActiveKey] = useState<string[]>([ALL_ATTRIBUTES_KEY]);
|
||||
|
||||
const {
|
||||
data: attributesData,
|
||||
isLoading: isLoadingAttributes,
|
||||
isError: isErrorAttributes,
|
||||
refetch: refetchAttributes,
|
||||
} = useGetMetricAttributes({
|
||||
metricName,
|
||||
});
|
||||
|
||||
const attributes = useMemo(() => attributesData?.data.attributes ?? [], [
|
||||
attributesData,
|
||||
]);
|
||||
const [activeKey, setActiveKey] = useState<string | string[]>(
|
||||
'all-attributes',
|
||||
);
|
||||
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
|
||||
@@ -221,7 +178,7 @@ function AllAttributes({
|
||||
attributes.filter(
|
||||
(attribute) =>
|
||||
attribute.key.toLowerCase().includes(searchString.toLowerCase()) ||
|
||||
attribute.values?.some((value) =>
|
||||
attribute.value.some((value) =>
|
||||
value.toLowerCase().includes(searchString.toLowerCase()),
|
||||
),
|
||||
),
|
||||
@@ -238,7 +195,7 @@ function AllAttributes({
|
||||
},
|
||||
value: {
|
||||
key: attribute.key,
|
||||
value: attribute.values,
|
||||
value: attribute.value,
|
||||
},
|
||||
}))
|
||||
: [],
|
||||
@@ -313,7 +270,6 @@ function AllAttributes({
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
disabled={isLoadingAttributes || isErrorAttributes}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
@@ -321,49 +277,25 @@ function AllAttributes({
|
||||
children: (
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
loading={isLoadingAttributes}
|
||||
tableLayout="fixed"
|
||||
dataSource={tableData}
|
||||
pagination={false}
|
||||
showHeader={false}
|
||||
className="metrics-accordion-content all-attributes-content"
|
||||
scroll={{ y: 600 }}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<AllAttributesEmptyText
|
||||
isErrorAttributes={isErrorAttributes}
|
||||
refetchAttributes={refetchAttributes}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
searchString,
|
||||
isLoadingAttributes,
|
||||
isErrorAttributes,
|
||||
columns,
|
||||
tableData,
|
||||
refetchAttributes,
|
||||
],
|
||||
[columns, tableData, searchString],
|
||||
);
|
||||
|
||||
if (isLoadingAttributes) {
|
||||
return (
|
||||
<div className="all-attributes-skeleton-container">
|
||||
<Skeleton active paragraph={{ rows: 8 }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
bordered
|
||||
className="metrics-accordion"
|
||||
className="metrics-accordion metrics-metadata-accordion"
|
||||
activeKey={activeKey}
|
||||
onChange={(keys): void => setActiveKey(keys as string[])}
|
||||
onChange={(keys): void => setActiveKey(keys)}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,84 +2,36 @@ import { useMemo } from 'react';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Dropdown, Typography } from 'antd';
|
||||
import { Skeleton } from 'antd/lib';
|
||||
import {
|
||||
useGetMetricAlerts,
|
||||
useGetMetricDashboards,
|
||||
} from 'api/generated/services/metrics';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
import { Bell, Grid } from 'lucide-react';
|
||||
import { pluralize } from 'utils/pluralize';
|
||||
|
||||
import { DashboardsAndAlertsPopoverProps } from './types';
|
||||
|
||||
function DashboardsAndAlertsPopover({
|
||||
metricName,
|
||||
alerts,
|
||||
dashboards,
|
||||
}: DashboardsAndAlertsPopoverProps): JSX.Element | null {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const params = useUrlQuery();
|
||||
|
||||
const {
|
||||
data: alertsData,
|
||||
isLoading: isLoadingAlerts,
|
||||
isError: isErrorAlerts,
|
||||
} = useGetMetricAlerts(
|
||||
{
|
||||
metricName,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled: !!metricName,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: dashboardsData,
|
||||
isLoading: isLoadingDashboards,
|
||||
isError: isErrorDashboards,
|
||||
} = useGetMetricDashboards(
|
||||
{
|
||||
metricName,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled: !!metricName,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const alerts = useMemo(() => {
|
||||
return alertsData?.data.alerts ?? [];
|
||||
}, [alertsData]);
|
||||
|
||||
const dashboards = useMemo(() => {
|
||||
const currentDashboards = dashboardsData?.data.dashboards ?? [];
|
||||
// Remove duplicate dashboards
|
||||
return currentDashboards.filter(
|
||||
(dashboard, index, self) =>
|
||||
index === self.findIndex((t) => t.dashboardId === dashboard.dashboardId),
|
||||
);
|
||||
}, [dashboardsData]);
|
||||
|
||||
const alertsPopoverContent = useMemo(() => {
|
||||
if (alerts && alerts.length > 0) {
|
||||
return alerts.map((alert) => ({
|
||||
key: alert.alertId,
|
||||
key: alert.alert_id,
|
||||
label: (
|
||||
<Typography.Link
|
||||
key={alert.alertId}
|
||||
key={alert.alert_id}
|
||||
onClick={(): void => {
|
||||
params.set(QueryParams.ruleId, alert.alertId);
|
||||
params.set(QueryParams.ruleId, alert.alert_id);
|
||||
history.push(`${ROUTES.ALERT_OVERVIEW}?${params.toString()}`);
|
||||
}}
|
||||
className="dashboards-popover-content-item"
|
||||
>
|
||||
{alert.alertName || alert.alertId}
|
||||
{alert.alert_name || alert.alert_id}
|
||||
</Typography.Link>
|
||||
),
|
||||
}));
|
||||
@@ -87,44 +39,41 @@ function DashboardsAndAlertsPopover({
|
||||
return null;
|
||||
}, [alerts, params]);
|
||||
|
||||
const uniqueDashboards = useMemo(
|
||||
() =>
|
||||
dashboards?.filter(
|
||||
(item, index, self) =>
|
||||
index === self.findIndex((t) => t.dashboard_id === item.dashboard_id),
|
||||
),
|
||||
[dashboards],
|
||||
);
|
||||
|
||||
const dashboardsPopoverContent = useMemo(() => {
|
||||
if (dashboards && dashboards.length > 0) {
|
||||
return dashboards.map((dashboard) => ({
|
||||
key: dashboard.dashboardId,
|
||||
if (uniqueDashboards && uniqueDashboards.length > 0) {
|
||||
return uniqueDashboards.map((dashboard) => ({
|
||||
key: dashboard.dashboard_id,
|
||||
label: (
|
||||
<Typography.Link
|
||||
key={dashboard.dashboardId}
|
||||
key={dashboard.dashboard_id}
|
||||
onClick={(): void => {
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, {
|
||||
dashboardId: dashboard.dashboardId,
|
||||
dashboardId: dashboard.dashboard_id,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
className="dashboards-popover-content-item"
|
||||
>
|
||||
{dashboard.dashboardName || dashboard.dashboardId}
|
||||
{dashboard.dashboard_name || dashboard.dashboard_id}
|
||||
</Typography.Link>
|
||||
),
|
||||
}));
|
||||
}
|
||||
return null;
|
||||
}, [dashboards, safeNavigate]);
|
||||
}, [uniqueDashboards, safeNavigate]);
|
||||
|
||||
if (isLoadingAlerts || isLoadingDashboards) {
|
||||
return (
|
||||
<div className="dashboards-and-alerts-popover-container">
|
||||
<Skeleton title={false} paragraph={{ rows: 1 }} active />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If there are no dashboards or alerts or both have errors, don't show the popover
|
||||
const hidePopover =
|
||||
(!dashboardsPopoverContent && !alertsPopoverContent) ||
|
||||
(isErrorAlerts && isErrorDashboards);
|
||||
if (hidePopover) {
|
||||
return <div className="dashboards-and-alerts-popover-container" />;
|
||||
if (!dashboardsPopoverContent && !alertsPopoverContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -143,7 +92,8 @@ function DashboardsAndAlertsPopover({
|
||||
>
|
||||
<Grid size={12} color={Color.BG_SIENNA_500} />
|
||||
<Typography.Text>
|
||||
{pluralize(dashboards.length, 'dashboard')}
|
||||
{uniqueDashboards?.length} dashboard
|
||||
{uniqueDashboards?.length === 1 ? '' : 's'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Dropdown>
|
||||
@@ -162,7 +112,7 @@ function DashboardsAndAlertsPopover({
|
||||
>
|
||||
<Bell size={12} color={Color.BG_SAKURA_500} />
|
||||
<Typography.Text>
|
||||
{pluralize(alerts.length, 'alert rule')}
|
||||
{alerts?.length} alert {alerts?.length === 1 ? 'rule' : 'rules'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Skeleton, Tooltip, Typography } from 'antd';
|
||||
import { useGetMetricHighlights } from 'api/generated/services/metrics';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
|
||||
import { HighlightsProps } from './types';
|
||||
import {
|
||||
formatNumberToCompactFormat,
|
||||
formatTimestampToReadableDate,
|
||||
} from './utils';
|
||||
|
||||
function Highlights({ metricName }: HighlightsProps): JSX.Element {
|
||||
const {
|
||||
data: metricHighlightsData,
|
||||
isLoading: isLoadingMetricHighlights,
|
||||
isError: isErrorMetricHighlights,
|
||||
refetch: refetchMetricHighlights,
|
||||
} = useGetMetricHighlights(
|
||||
{
|
||||
metricName,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled: !!metricName,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const metricHighlights = metricHighlightsData?.data;
|
||||
|
||||
const timeSeriesActive = formatNumberToCompactFormat(
|
||||
metricHighlights?.activeTimeSeries,
|
||||
);
|
||||
const timeSeriesTotal = formatNumberToCompactFormat(
|
||||
metricHighlights?.totalTimeSeries,
|
||||
);
|
||||
const lastReceivedText = formatTimestampToReadableDate(
|
||||
metricHighlights?.lastReceived,
|
||||
);
|
||||
|
||||
if (isLoadingMetricHighlights) {
|
||||
return (
|
||||
<div
|
||||
className="metric-details-content-grid"
|
||||
data-testid="metric-highlights-loading-state"
|
||||
>
|
||||
<Skeleton title={false} paragraph={{ rows: 2 }} active />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isErrorMetricHighlights) {
|
||||
return (
|
||||
<div className="metric-details-content-grid">
|
||||
<div
|
||||
className="metric-highlights-error-state"
|
||||
data-testid="metric-highlights-error-state"
|
||||
>
|
||||
<InfoIcon size={16} color={Color.BG_CHERRY_500} />
|
||||
<Typography.Text>
|
||||
Something went wrong while fetching metric highlights
|
||||
</Typography.Text>
|
||||
<Button
|
||||
type="link"
|
||||
size="large"
|
||||
onClick={(): void => {
|
||||
refetchMetricHighlights();
|
||||
}}
|
||||
>
|
||||
Retry ?
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="metric-details-content-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text type="secondary" className="metric-details-grid-label">
|
||||
SAMPLES
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" className="metric-details-grid-label">
|
||||
TIME SERIES
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" className="metric-details-grid-label">
|
||||
LAST RECEIVED
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="values-row">
|
||||
<Typography.Text
|
||||
className="metric-details-grid-value"
|
||||
data-testid="metric-highlights-data-points"
|
||||
>
|
||||
<Tooltip title={metricHighlights?.dataPoints?.toLocaleString()}>
|
||||
{formatNumberIntoHumanReadableFormat(metricHighlights?.dataPoints ?? 0)}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
className="metric-details-grid-value"
|
||||
data-testid="metric-highlights-time-series-total"
|
||||
>
|
||||
<Tooltip
|
||||
title="Active time series are those that have received data points in the last 1
|
||||
hour."
|
||||
placement="top"
|
||||
>
|
||||
<span>{`${timeSeriesTotal} total ⎯ ${timeSeriesActive} active`}</span>
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text
|
||||
className="metric-details-grid-value"
|
||||
data-testid="metric-highlights-last-received"
|
||||
>
|
||||
<Tooltip title={lastReceivedText}>{lastReceivedText}</Tooltip>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Highlights;
|
||||
@@ -1,58 +1,45 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Button, Collapse, Input, Select, Skeleton, Typography } from 'antd';
|
||||
import { Button, Collapse, Input, Select, Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import {
|
||||
invalidateGetMetricMetadata,
|
||||
invalidateListMetrics,
|
||||
useUpdateMetricMetadata,
|
||||
} from 'api/generated/services/metrics';
|
||||
import {
|
||||
MetrictypesTemporalityDTO,
|
||||
MetrictypesTypeDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { AxiosError } from 'axios';
|
||||
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import { UpdateMetricMetadataProps } from 'api/metricsExplorer/updateMetricMetadata';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
|
||||
import FieldRenderer from 'container/LogDetailedView/FieldRenderer';
|
||||
import { DataType } from 'container/LogDetailedView/TableView';
|
||||
import { useUpdateMetricMetadata } from 'hooks/metricsExplorer/useUpdateMetricMetadata';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { Edit2, Save, X } from 'lucide-react';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import { MetricTypeViewRenderer } from '../Summary/utils';
|
||||
import {
|
||||
METRIC_METADATA_KEYS,
|
||||
METRIC_METADATA_TEMPORALITY_OPTIONS,
|
||||
METRIC_METADATA_TYPE_OPTIONS,
|
||||
METRIC_METADATA_UPDATE_ERROR_MESSAGE,
|
||||
} from './constants';
|
||||
import MetricDetailsErrorState from './MetricDetailsErrorState';
|
||||
import { MetadataProps, MetricMetadataFormState, TableFields } from './types';
|
||||
import { transformUpdateMetricMetadataRequest } from './utils';
|
||||
METRIC_TYPE_LABEL_MAP,
|
||||
METRIC_TYPE_VALUES_MAP,
|
||||
} from '../Summary/constants';
|
||||
import { MetricTypeRenderer } from '../Summary/utils';
|
||||
import { METRIC_METADATA_KEYS } from './constants';
|
||||
import { MetadataProps } from './types';
|
||||
import { determineIsMonotonic } from './utils';
|
||||
|
||||
function Metadata({
|
||||
metricName,
|
||||
metadata,
|
||||
isErrorMetricMetadata,
|
||||
isLoadingMetricMetadata,
|
||||
refetchMetricMetadata,
|
||||
refetchMetricDetails,
|
||||
}: MetadataProps): JSX.Element {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const [
|
||||
metricMetadataState,
|
||||
setMetricMetadataState,
|
||||
] = useState<MetricMetadataFormState>({
|
||||
type: MetrictypesTypeDTO.sum,
|
||||
description: '',
|
||||
temporality: MetrictypesTemporalityDTO.unspecified,
|
||||
unit: '',
|
||||
isMonotonic: false,
|
||||
metricMetadata,
|
||||
setMetricMetadata,
|
||||
] = useState<UpdateMetricMetadataProps>({
|
||||
metricType: metadata?.metric_type || MetricType.SUM,
|
||||
description: metadata?.description || '',
|
||||
temporality: metadata?.temporality,
|
||||
unit: metadata?.unit,
|
||||
});
|
||||
const { notifications } = useNotifications();
|
||||
const {
|
||||
@@ -64,135 +51,110 @@ function Metadata({
|
||||
);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Initialize state from metadata api data
|
||||
useEffect(() => {
|
||||
if (metadata) {
|
||||
setMetricMetadataState({
|
||||
type: metadata.type,
|
||||
description: metadata.description,
|
||||
temporality: metadata.temporality,
|
||||
unit: metadata.unit,
|
||||
isMonotonic: metadata.isMonotonic,
|
||||
});
|
||||
}
|
||||
}, [metadata]);
|
||||
|
||||
const tableData = useMemo(
|
||||
() =>
|
||||
metadata
|
||||
? Object.keys(metadata).map((key) => ({
|
||||
key,
|
||||
value: {
|
||||
value: metadata[key as keyof typeof metadata],
|
||||
? Object.keys({
|
||||
...metadata,
|
||||
temporality: metadata?.temporality,
|
||||
})
|
||||
// Filter out monotonic as user input is not required
|
||||
.filter((key) => key !== 'monotonic')
|
||||
.map((key) => ({
|
||||
key,
|
||||
},
|
||||
}))
|
||||
value: {
|
||||
value: metadata[key as keyof typeof metadata],
|
||||
key,
|
||||
},
|
||||
}))
|
||||
: [],
|
||||
[metadata],
|
||||
);
|
||||
|
||||
// Render un-editable field value
|
||||
const renderUneditableField = useCallback(
|
||||
(key: keyof MetricMetadataFormState, value: string) => {
|
||||
if (isErrorMetricMetadata) {
|
||||
return <FieldRenderer field="-" />;
|
||||
}
|
||||
if (key === TableFields.TYPE) {
|
||||
return <MetricTypeViewRenderer type={value as MetrictypesTypeDTO} />;
|
||||
}
|
||||
if (key === TableFields.IS_MONOTONIC) {
|
||||
return <FieldRenderer field={value ? 'Yes' : 'No'} />;
|
||||
}
|
||||
if (key === TableFields.Temporality) {
|
||||
const temporality = METRIC_METADATA_TEMPORALITY_OPTIONS.find(
|
||||
(option) => option.value === value,
|
||||
);
|
||||
return <FieldRenderer field={temporality?.label || '-'} />;
|
||||
}
|
||||
let fieldValue = value;
|
||||
if (key === TableFields.UNIT) {
|
||||
fieldValue = getUniversalNameFromMetricUnit(value);
|
||||
}
|
||||
return <FieldRenderer field={fieldValue || '-'} />;
|
||||
},
|
||||
[isErrorMetricMetadata],
|
||||
);
|
||||
const renderUneditableField = useCallback((key: string, value: string) => {
|
||||
if (key === 'metric_type') {
|
||||
return <MetricTypeRenderer type={value as MetricType} />;
|
||||
}
|
||||
let fieldValue = value;
|
||||
if (key === 'unit') {
|
||||
fieldValue = getUniversalNameFromMetricUnit(value);
|
||||
}
|
||||
return <FieldRenderer field={fieldValue || '-'} />;
|
||||
}, []);
|
||||
|
||||
const renderColumnValue = useCallback(
|
||||
(field: {
|
||||
value: string;
|
||||
key: keyof MetricMetadataFormState;
|
||||
}): JSX.Element => {
|
||||
(field: { value: string; key: string }): JSX.Element => {
|
||||
if (!isEditing) {
|
||||
return renderUneditableField(field.key, field.value);
|
||||
}
|
||||
|
||||
// Don't allow editing of unit if it's already set
|
||||
const metricUnitAlreadySet =
|
||||
field.key === TableFields.UNIT && Boolean(metadata?.unit);
|
||||
const metricUnitAlreadySet = field.key === 'unit' && Boolean(metadata?.unit);
|
||||
if (metricUnitAlreadySet) {
|
||||
return renderUneditableField(field.key, field.value);
|
||||
}
|
||||
|
||||
// Monotonic is not editable
|
||||
if (field.key === TableFields.IS_MONOTONIC) {
|
||||
return renderUneditableField(field.key, field.value);
|
||||
}
|
||||
|
||||
if (field.key === TableFields.TYPE) {
|
||||
if (field.key === 'metric_type') {
|
||||
return (
|
||||
<Select
|
||||
data-testid="metric-type-select"
|
||||
options={METRIC_METADATA_TYPE_OPTIONS}
|
||||
value={metricMetadataState.type}
|
||||
options={Object.entries(METRIC_TYPE_VALUES_MAP).map(([key]) => ({
|
||||
value: key,
|
||||
label: METRIC_TYPE_LABEL_MAP[key as MetricType],
|
||||
}))}
|
||||
value={metricMetadata.metricType}
|
||||
onChange={(value): void => {
|
||||
setMetricMetadataState((prev) => ({
|
||||
setMetricMetadata((prev) => ({
|
||||
...prev,
|
||||
type: value,
|
||||
metricType: value as MetricType,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (field.key === TableFields.UNIT) {
|
||||
if (field.key === 'unit') {
|
||||
return (
|
||||
<YAxisUnitSelector
|
||||
value={metricMetadataState.unit}
|
||||
value={metricMetadata.unit}
|
||||
onChange={(value): void => {
|
||||
setMetricMetadataState((prev) => ({ ...prev, unit: value }));
|
||||
setMetricMetadata((prev) => ({ ...prev, unit: value }));
|
||||
}}
|
||||
data-testid="unit-select"
|
||||
source={YAxisSource.EXPLORER}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (field.key === TableFields.Temporality) {
|
||||
const temporalityValue =
|
||||
metricMetadataState.temporality === MetrictypesTemporalityDTO.unspecified
|
||||
? undefined
|
||||
: metricMetadataState.temporality;
|
||||
if (field.key === 'temporality') {
|
||||
return (
|
||||
<Select
|
||||
data-testid="temporality-select"
|
||||
options={METRIC_METADATA_TEMPORALITY_OPTIONS}
|
||||
value={temporalityValue}
|
||||
options={Object.values(Temporality).map((key) => ({
|
||||
value: key,
|
||||
label: key,
|
||||
}))}
|
||||
value={metricMetadata.temporality}
|
||||
onChange={(value): void => {
|
||||
setMetricMetadataState((prev) => ({
|
||||
setMetricMetadata((prev) => ({
|
||||
...prev,
|
||||
temporality: value,
|
||||
temporality: value as Temporality,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (field.key === TableFields.DESCRIPTION) {
|
||||
if (field.key === 'description') {
|
||||
return (
|
||||
<Input
|
||||
data-testid="description-input"
|
||||
name={field.key}
|
||||
defaultValue={metricMetadataState.description}
|
||||
defaultValue={
|
||||
metricMetadata[
|
||||
field.key as Exclude<keyof UpdateMetricMetadataProps, 'isMonotonic'>
|
||||
]
|
||||
}
|
||||
onChange={(e): void => {
|
||||
setMetricMetadataState((prev) => ({
|
||||
setMetricMetadata((prev) => ({
|
||||
...prev,
|
||||
[field.key]: e.target.value,
|
||||
}));
|
||||
@@ -202,7 +164,7 @@ function Metadata({
|
||||
}
|
||||
return <FieldRenderer field="-" />;
|
||||
},
|
||||
[isEditing, metadata?.unit, metricMetadataState, renderUneditableField],
|
||||
[isEditing, metadata?.unit, metricMetadata, renderUneditableField],
|
||||
);
|
||||
|
||||
const columns: ColumnsType<DataType> = useMemo(
|
||||
@@ -239,61 +201,52 @@ function Metadata({
|
||||
const handleSave = useCallback(() => {
|
||||
updateMetricMetadata(
|
||||
{
|
||||
pathParams: {
|
||||
metricName,
|
||||
metricName,
|
||||
payload: {
|
||||
...metricMetadata,
|
||||
isMonotonic: determineIsMonotonic(
|
||||
metricMetadata.metricType,
|
||||
metricMetadata.temporality,
|
||||
),
|
||||
},
|
||||
data: transformUpdateMetricMetadataRequest(metricName, metricMetadataState),
|
||||
},
|
||||
{
|
||||
onSuccess: (): void => {
|
||||
logEvent(MetricsExplorerEvents.MetricMetadataUpdated, {
|
||||
[MetricsExplorerEventKeys.MetricName]: metricName,
|
||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
||||
});
|
||||
notifications.success({
|
||||
message: 'Metadata updated successfully',
|
||||
});
|
||||
setIsEditing(false);
|
||||
invalidateListMetrics(queryClient);
|
||||
invalidateGetMetricMetadata(queryClient, {
|
||||
metricName,
|
||||
});
|
||||
onSuccess: (response): void => {
|
||||
if (response?.statusCode === 200) {
|
||||
logEvent(MetricsExplorerEvents.MetricMetadataUpdated, {
|
||||
[MetricsExplorerEventKeys.MetricName]: metricName,
|
||||
[MetricsExplorerEventKeys.Tab]: 'summary',
|
||||
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
||||
});
|
||||
notifications.success({
|
||||
message: 'Metadata updated successfully',
|
||||
});
|
||||
refetchMetricDetails();
|
||||
setIsEditing(false);
|
||||
queryClient.invalidateQueries(['metricsList']);
|
||||
} else {
|
||||
notifications.error({
|
||||
message:
|
||||
'Failed to update metadata, please try again. If the issue persists, please contact support.',
|
||||
});
|
||||
}
|
||||
},
|
||||
onError: (error): void => {
|
||||
const errorMessage = (error as AxiosError<RenderErrorResponseDTO>).response
|
||||
?.data.error?.message;
|
||||
onError: (): void =>
|
||||
notifications.error({
|
||||
message: errorMessage || METRIC_METADATA_UPDATE_ERROR_MESSAGE,
|
||||
});
|
||||
},
|
||||
message:
|
||||
'Failed to update metadata, please try again. If the issue persists, please contact support.',
|
||||
}),
|
||||
},
|
||||
);
|
||||
}, [
|
||||
updateMetricMetadata,
|
||||
metricName,
|
||||
metricMetadataState,
|
||||
metricMetadata,
|
||||
notifications,
|
||||
refetchMetricDetails,
|
||||
queryClient,
|
||||
]);
|
||||
|
||||
const cancelEdit = useCallback(
|
||||
(e: React.MouseEvent<HTMLElement, MouseEvent>): void => {
|
||||
e.stopPropagation();
|
||||
if (metadata) {
|
||||
setMetricMetadataState({
|
||||
type: metadata.type,
|
||||
description: metadata.description,
|
||||
unit: metadata.unit,
|
||||
temporality: metadata.temporality,
|
||||
isMonotonic: metadata.isMonotonic,
|
||||
});
|
||||
}
|
||||
setIsEditing(false);
|
||||
},
|
||||
[metadata],
|
||||
);
|
||||
|
||||
const actionButton = useMemo(() => {
|
||||
if (isEditing) {
|
||||
return (
|
||||
@@ -301,7 +254,10 @@ function Metadata({
|
||||
<Button
|
||||
className="action-button"
|
||||
type="text"
|
||||
onClick={cancelEdit}
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
setIsEditing(false);
|
||||
}}
|
||||
disabled={isUpdatingMetricsMetadata}
|
||||
>
|
||||
<X size={14} />
|
||||
@@ -322,9 +278,6 @@ function Metadata({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (isErrorMetricMetadata) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="action-menu">
|
||||
<Button
|
||||
@@ -341,13 +294,7 @@ function Metadata({
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}, [
|
||||
isEditing,
|
||||
isErrorMetricMetadata,
|
||||
isUpdatingMetricsMetadata,
|
||||
cancelEdit,
|
||||
handleSave,
|
||||
]);
|
||||
}, [handleSave, isEditing, isUpdatingMetricsMetadata]);
|
||||
|
||||
const items = useMemo(
|
||||
() => [
|
||||
@@ -359,14 +306,7 @@ function Metadata({
|
||||
</div>
|
||||
),
|
||||
key: 'metric-metadata',
|
||||
children: isErrorMetricMetadata ? (
|
||||
<div className="metric-metadata-error-state">
|
||||
<MetricDetailsErrorState
|
||||
refetch={refetchMetricMetadata}
|
||||
errorMessage="Something went wrong while fetching metric metadata"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
children: (
|
||||
<ResizeTable
|
||||
columns={columns}
|
||||
tableLayout="fixed"
|
||||
@@ -378,23 +318,9 @@ function Metadata({
|
||||
),
|
||||
},
|
||||
],
|
||||
[
|
||||
actionButton,
|
||||
columns,
|
||||
isErrorMetricMetadata,
|
||||
refetchMetricMetadata,
|
||||
tableData,
|
||||
],
|
||||
[actionButton, columns, tableData],
|
||||
);
|
||||
|
||||
if (isLoadingMetricMetadata) {
|
||||
return (
|
||||
<div className="metrics-metadata-skeleton-container">
|
||||
<Skeleton active paragraph={{ rows: 8 }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Collapse
|
||||
bordered
|
||||
|
||||
@@ -38,12 +38,7 @@
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.metrics-metadata-error {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
.metric-details-content-grid {
|
||||
height: 50px;
|
||||
.labels-row,
|
||||
.values-row {
|
||||
display: grid;
|
||||
@@ -52,18 +47,6 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metric-highlights-error-state {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
.ant-btn {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.labels-row {
|
||||
margin-bottom: 8px;
|
||||
|
||||
@@ -89,7 +72,6 @@
|
||||
.dashboards-and-alerts-popover-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
height: 32px;
|
||||
|
||||
.dashboards-and-alerts-popover {
|
||||
border-radius: 20px;
|
||||
@@ -120,19 +102,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-metadata-skeleton-container {
|
||||
height: 330px;
|
||||
}
|
||||
|
||||
.all-attributes-skeleton-container {
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.metrics-accordion {
|
||||
.all-attributes-error-state {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.ant-table-body {
|
||||
&::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
@@ -178,6 +148,7 @@
|
||||
|
||||
.all-attributes-search-input {
|
||||
width: 300px;
|
||||
border: 1px solid var(--bg-slate-300);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +161,6 @@
|
||||
.ant-typography:first-child {
|
||||
font-family: 'Geist Mono';
|
||||
color: var(--bg-robin-400);
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.all-attributes-contribution {
|
||||
@@ -247,10 +217,6 @@
|
||||
|
||||
.ant-collapse-content-box {
|
||||
padding: 0;
|
||||
|
||||
.metric-metadata-error-state {
|
||||
height: 267px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-collapse-header {
|
||||
@@ -271,7 +237,6 @@
|
||||
}
|
||||
|
||||
.metric-metadata-value {
|
||||
height: 67px;
|
||||
background: rgba(22, 25, 34, 0.4);
|
||||
overflow-x: scroll;
|
||||
.field-renderer-container {
|
||||
@@ -365,26 +330,18 @@
|
||||
.metric-details-content {
|
||||
.metrics-accordion {
|
||||
.metrics-accordion-header {
|
||||
.action-menu {
|
||||
.action-button {
|
||||
.ant-typography {
|
||||
color: var(--bg-slate-400);
|
||||
}
|
||||
.action-button {
|
||||
.ant-typography {
|
||||
color: var(--bg-slate-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metrics-accordion-content {
|
||||
.metric-metadata-key {
|
||||
.field-renderer-container {
|
||||
.label {
|
||||
color: var(--bg-slate-300);
|
||||
}
|
||||
}
|
||||
|
||||
.all-attributes-key {
|
||||
.ant-typography:last-child {
|
||||
color: var(--bg-vanilla-200);
|
||||
color: var(--bg-slate-400);
|
||||
background-color: var(--bg-robin-300);
|
||||
}
|
||||
}
|
||||
@@ -438,13 +395,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metric-details-error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Typography } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Drawer,
|
||||
Empty,
|
||||
Skeleton,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { useGetMetricMetadata } from 'api/generated/services/metrics';
|
||||
import { useGetMetricDetails } from 'hooks/metricsExplorer/useGetMetricDetails';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { Compass, Crosshair, X } from 'lucide-react';
|
||||
|
||||
@@ -11,12 +19,16 @@ import ROUTES from '../../../constants/routes';
|
||||
import { useHandleExplorerTabChange } from '../../../hooks/useHandleExplorerTabChange';
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
import { isInspectEnabled } from '../Inspect/utils';
|
||||
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
|
||||
import AllAttributes from './AllAttributes';
|
||||
import DashboardsAndAlertsPopover from './DashboardsAndAlertsPopover';
|
||||
import Highlights from './Highlights';
|
||||
import Metadata from './Metadata';
|
||||
import { MetricDetailsProps } from './types';
|
||||
import { getMetricDetailsQuery } from './utils';
|
||||
import {
|
||||
formatNumberToCompactFormat,
|
||||
formatTimestampToReadableDate,
|
||||
getMetricDetailsQuery,
|
||||
} from './utils';
|
||||
|
||||
import './MetricDetails.styles.scss';
|
||||
import '../Summary/Summary.styles.scss';
|
||||
@@ -31,49 +43,55 @@ function MetricDetails({
|
||||
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||
|
||||
const {
|
||||
data: metricMetadataResponse,
|
||||
isLoading: isLoadingMetricMetadata,
|
||||
isError: isErrorMetricMetadata,
|
||||
refetch: refetchMetricMetadata,
|
||||
} = useGetMetricMetadata(
|
||||
{
|
||||
metricName,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
enabled: !!metricName,
|
||||
},
|
||||
},
|
||||
);
|
||||
data,
|
||||
isLoading,
|
||||
isFetching,
|
||||
error: metricDetailsError,
|
||||
refetch: refetchMetricDetails,
|
||||
} = useGetMetricDetails(metricName ?? '', {
|
||||
enabled: !!metricName,
|
||||
});
|
||||
|
||||
const metadata = useMemo(() => {
|
||||
if (!metricMetadataResponse) {
|
||||
const metric = data?.payload?.data;
|
||||
|
||||
const lastReceived = useMemo(() => {
|
||||
if (!metric) {
|
||||
return null;
|
||||
}
|
||||
const {
|
||||
type,
|
||||
description,
|
||||
unit,
|
||||
temporality,
|
||||
isMonotonic,
|
||||
} = metricMetadataResponse.data;
|
||||
return formatTimestampToReadableDate(metric.lastReceived);
|
||||
}, [metric]);
|
||||
|
||||
return {
|
||||
type,
|
||||
description,
|
||||
unit,
|
||||
temporality,
|
||||
isMonotonic,
|
||||
};
|
||||
}, [metricMetadataResponse]);
|
||||
const showInspectFeature = useMemo(
|
||||
() => isInspectEnabled(metric?.metadata?.metric_type),
|
||||
[metric],
|
||||
);
|
||||
|
||||
const showInspectFeature = useMemo(() => isInspectEnabled(metadata?.type), [
|
||||
metadata?.type,
|
||||
]);
|
||||
const isMetricDetailsLoading = isLoading || isFetching;
|
||||
|
||||
const timeSeries = useMemo(() => {
|
||||
if (!metric) {
|
||||
return null;
|
||||
}
|
||||
const timeSeriesActive = formatNumberToCompactFormat(metric.timeSeriesActive);
|
||||
const timeSeriesTotal = formatNumberToCompactFormat(metric.timeSeriesTotal);
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title="Active time series are those that have received data points in the last 1
|
||||
hour."
|
||||
placement="top"
|
||||
>
|
||||
<span>{`${timeSeriesTotal} total ⎯ ${timeSeriesActive} active`}</span>
|
||||
</Tooltip>
|
||||
);
|
||||
}, [metric]);
|
||||
|
||||
const goToMetricsExplorerwithSelectedMetric = useCallback(() => {
|
||||
if (metricName) {
|
||||
const compositeQuery = getMetricDetailsQuery(metricName, metadata?.type);
|
||||
const compositeQuery = getMetricDetailsQuery(
|
||||
metricName,
|
||||
metric?.metadata?.metric_type,
|
||||
);
|
||||
handleExplorerTabChange(
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
{
|
||||
@@ -89,7 +107,9 @@ function MetricDetails({
|
||||
[MetricsExplorerEventKeys.Modal]: 'metric-details',
|
||||
});
|
||||
}
|
||||
}, [metricName, handleExplorerTabChange, metadata?.type]);
|
||||
}, [metricName, handleExplorerTabChange, metric?.metadata?.metric_type]);
|
||||
|
||||
const isMetricDetailsError = metricDetailsError || !metric;
|
||||
|
||||
useEffect(() => {
|
||||
logEvent(MetricsExplorerEvents.ModalOpened, {
|
||||
@@ -97,9 +117,6 @@ function MetricDetails({
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isActionButtonDisabled =
|
||||
!metricName || isLoadingMetricMetadata || isErrorMetricMetadata;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="60%"
|
||||
@@ -107,13 +124,13 @@ function MetricDetails({
|
||||
<div className="metric-details-header">
|
||||
<div className="metric-details-title">
|
||||
<Divider type="vertical" />
|
||||
<Typography.Text>{metricName}</Typography.Text>
|
||||
<Typography.Text>{metric?.name}</Typography.Text>
|
||||
</div>
|
||||
<div className="metric-details-header-buttons">
|
||||
<Button
|
||||
onClick={goToMetricsExplorerwithSelectedMetric}
|
||||
icon={<Compass size={16} />}
|
||||
disabled={isActionButtonDisabled}
|
||||
disabled={!metricName}
|
||||
data-testid="open-in-explorer-button"
|
||||
>
|
||||
Open in Explorer
|
||||
@@ -123,11 +140,10 @@ function MetricDetails({
|
||||
<Button
|
||||
className="inspect-metrics-button"
|
||||
aria-label="Inspect Metric"
|
||||
disabled={isActionButtonDisabled}
|
||||
icon={<Crosshair size={18} />}
|
||||
onClick={(): void => {
|
||||
if (metricName) {
|
||||
openInspectModal(metricName);
|
||||
if (metric?.name) {
|
||||
openInspectModal(metric.name);
|
||||
}
|
||||
}}
|
||||
data-testid="inspect-metric-button"
|
||||
@@ -147,18 +163,60 @@ function MetricDetails({
|
||||
destroyOnClose
|
||||
closeIcon={<X size={16} />}
|
||||
>
|
||||
<div className="metric-details-content">
|
||||
<Highlights metricName={metricName} />
|
||||
<DashboardsAndAlertsPopover metricName={metricName} />
|
||||
<Metadata
|
||||
metricName={metricName}
|
||||
metadata={metadata}
|
||||
isErrorMetricMetadata={isErrorMetricMetadata}
|
||||
isLoadingMetricMetadata={isLoadingMetricMetadata}
|
||||
refetchMetricMetadata={refetchMetricMetadata}
|
||||
/>
|
||||
<AllAttributes metricName={metricName} metricType={metadata?.type} />
|
||||
</div>
|
||||
{isMetricDetailsLoading && (
|
||||
<div data-testid="metric-details-skeleton">
|
||||
<Skeleton active />
|
||||
</div>
|
||||
)}
|
||||
{isMetricDetailsError && !isMetricDetailsLoading && (
|
||||
<Empty description="Error fetching metric details" />
|
||||
)}
|
||||
{!isMetricDetailsLoading && !isMetricDetailsError && (
|
||||
<div className="metric-details-content">
|
||||
<div className="metric-details-content-grid">
|
||||
<div className="labels-row">
|
||||
<Typography.Text type="secondary" className="metric-details-grid-label">
|
||||
SAMPLES
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" className="metric-details-grid-label">
|
||||
TIME SERIES
|
||||
</Typography.Text>
|
||||
<Typography.Text type="secondary" className="metric-details-grid-label">
|
||||
LAST RECEIVED
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<div className="values-row">
|
||||
<Typography.Text className="metric-details-grid-value">
|
||||
<Tooltip title={metric?.samples.toLocaleString()}>
|
||||
{formatNumberIntoHumanReadableFormat(metric?.samples)}
|
||||
</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="metric-details-grid-value">
|
||||
<Tooltip title={timeSeries}>{timeSeries}</Tooltip>
|
||||
</Typography.Text>
|
||||
<Typography.Text className="metric-details-grid-value">
|
||||
<Tooltip title={lastReceived}>{lastReceived}</Tooltip>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
<DashboardsAndAlertsPopover
|
||||
dashboards={metric.dashboards}
|
||||
alerts={metric.alerts}
|
||||
/>
|
||||
<Metadata
|
||||
metricName={metric?.name}
|
||||
metadata={metric.metadata}
|
||||
refetchMetricDetails={refetchMetricDetails}
|
||||
/>
|
||||
{metric.attributes && (
|
||||
<AllAttributes
|
||||
metricName={metric?.name}
|
||||
attributes={metric.attributes}
|
||||
metricType={metric?.metadata?.metric_type}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Typography } from 'antd';
|
||||
import { InfoIcon } from 'lucide-react';
|
||||
|
||||
import { MetricDetailsErrorStateProps } from './types';
|
||||
|
||||
function MetricDetailsErrorState({
|
||||
refetch,
|
||||
errorMessage,
|
||||
}: MetricDetailsErrorStateProps): JSX.Element {
|
||||
return (
|
||||
<div className="metric-details-error-state">
|
||||
<InfoIcon size={20} color={Color.BG_CHERRY_500} />
|
||||
<Typography.Text>{errorMessage}</Typography.Text>
|
||||
{refetch && <Button onClick={refetch}>Retry</Button>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MetricDetailsErrorState;
|
||||
@@ -1,13 +1,11 @@
|
||||
import * as reactUseHooks from 'react-use';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import * as useHandleExplorerTabChange from 'hooks/useHandleExplorerTabChange';
|
||||
import { userEvent } from 'tests/test-utils';
|
||||
|
||||
import { MetricDetailsAttribute } from '../../../../api/metricsExplorer/getMetricDetails';
|
||||
import ROUTES from '../../../../constants/routes';
|
||||
import AllAttributes, { AllAttributesValue } from '../AllAttributes';
|
||||
import { getMockMetricAttributesData, MOCK_METRIC_NAME } from './testUtlls';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
@@ -22,28 +20,33 @@ jest
|
||||
handleExplorerTabChange: mockHandleExplorerTabChange,
|
||||
});
|
||||
|
||||
const mockMetricName = 'test-metric';
|
||||
const mockMetricType = MetricType.GAUGE;
|
||||
const mockAttributes: MetricDetailsAttribute[] = [
|
||||
{
|
||||
key: 'attribute1',
|
||||
value: ['value1', 'value2'],
|
||||
valueCount: 2,
|
||||
},
|
||||
{
|
||||
key: 'attribute2',
|
||||
value: ['value3'],
|
||||
valueCount: 1,
|
||||
},
|
||||
];
|
||||
|
||||
const mockUseCopyToClipboard = jest.fn();
|
||||
jest
|
||||
.spyOn(reactUseHooks, 'useCopyToClipboard')
|
||||
.mockReturnValue([{ value: 'value1' }, mockUseCopyToClipboard] as any);
|
||||
|
||||
const useGetMetricAttributesMock = jest.spyOn(
|
||||
metricsExplorerHooks,
|
||||
'useGetMetricAttributes',
|
||||
);
|
||||
|
||||
describe('AllAttributes', () => {
|
||||
beforeEach(() => {
|
||||
useGetMetricAttributesMock.mockReturnValue({
|
||||
...getMockMetricAttributesData(),
|
||||
});
|
||||
});
|
||||
|
||||
it('renders attributes section with title', () => {
|
||||
render(
|
||||
<AllAttributes
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricType={MetrictypesTypeDTO.gauge}
|
||||
metricName={mockMetricName}
|
||||
attributes={mockAttributes}
|
||||
metricType={mockMetricType}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -53,8 +56,9 @@ describe('AllAttributes', () => {
|
||||
it('renders all attribute keys and values', () => {
|
||||
render(
|
||||
<AllAttributes
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricType={MetrictypesTypeDTO.gauge}
|
||||
metricName={mockMetricName}
|
||||
attributes={mockAttributes}
|
||||
metricType={mockMetricType}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -71,8 +75,9 @@ describe('AllAttributes', () => {
|
||||
it('renders value counts correctly', () => {
|
||||
render(
|
||||
<AllAttributes
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricType={MetrictypesTypeDTO.gauge}
|
||||
metricName={mockMetricName}
|
||||
attributes={mockAttributes}
|
||||
metricType={mockMetricType}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -81,44 +86,41 @@ describe('AllAttributes', () => {
|
||||
});
|
||||
|
||||
it('handles empty attributes array', () => {
|
||||
useGetMetricAttributesMock.mockReturnValue({
|
||||
...getMockMetricAttributesData({
|
||||
data: {
|
||||
attributes: [],
|
||||
totalKeys: 0,
|
||||
},
|
||||
}),
|
||||
});
|
||||
render(
|
||||
<AllAttributes
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricType={MetrictypesTypeDTO.gauge}
|
||||
metricName={mockMetricName}
|
||||
attributes={[]}
|
||||
metricType={mockMetricType}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('All Attributes')).toBeInTheDocument();
|
||||
expect(screen.getByText('No attributes found')).toBeInTheDocument();
|
||||
expect(screen.queryByText('No data')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('clicking on an attribute key opens the explorer with the attribute filter applied', async () => {
|
||||
it('clicking on an attribute key opens the explorer with the attribute filter applied', () => {
|
||||
render(
|
||||
<AllAttributes
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricType={MetrictypesTypeDTO.gauge}
|
||||
metricName={mockMetricName}
|
||||
attributes={mockAttributes}
|
||||
metricType={mockMetricType}
|
||||
/>,
|
||||
);
|
||||
await userEvent.click(screen.getByText('attribute1'));
|
||||
fireEvent.click(screen.getByText('attribute1'));
|
||||
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('filters attributes based on search input', async () => {
|
||||
it('filters attributes based on search input', () => {
|
||||
render(
|
||||
<AllAttributes
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricType={MetrictypesTypeDTO.gauge}
|
||||
metricName={mockMetricName}
|
||||
attributes={mockAttributes}
|
||||
metricType={mockMetricType}
|
||||
/>,
|
||||
);
|
||||
await userEvent.type(screen.getByPlaceholderText('Search'), 'value1');
|
||||
fireEvent.change(screen.getByPlaceholderText('Search'), {
|
||||
target: { value: 'value1' },
|
||||
});
|
||||
|
||||
expect(screen.getByText('attribute1')).toBeInTheDocument();
|
||||
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||
@@ -142,7 +144,7 @@ describe('AllAttributesValue', () => {
|
||||
expect(screen.getByText('value2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('loads more attributes when show more button is clicked', async () => {
|
||||
it('loads more attributes when show more button is clicked', () => {
|
||||
render(
|
||||
<AllAttributesValue
|
||||
filterKey="attribute1"
|
||||
@@ -153,7 +155,7 @@ describe('AllAttributesValue', () => {
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByText('value6')).not.toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('Show More'));
|
||||
fireEvent.click(screen.getByText('Show More'));
|
||||
expect(screen.getByText('value6')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -170,7 +172,7 @@ describe('AllAttributesValue', () => {
|
||||
expect(screen.queryByText('Show More')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('copy button should copy the attribute value to the clipboard', async () => {
|
||||
it('copy button should copy the attribute value to the clipboard', () => {
|
||||
render(
|
||||
<AllAttributesValue
|
||||
filterKey="attribute1"
|
||||
@@ -181,13 +183,13 @@ describe('AllAttributesValue', () => {
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('value1'));
|
||||
fireEvent.click(screen.getByText('value1'));
|
||||
expect(screen.getByText('Copy Attribute')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('Copy Attribute'));
|
||||
fireEvent.click(screen.getByText('Copy Attribute'));
|
||||
expect(mockUseCopyToClipboard).toHaveBeenCalledWith('value1');
|
||||
});
|
||||
|
||||
it('explorer button should go to metrics explore with the attribute filter applied', async () => {
|
||||
it('explorer button should go to metrics explore with the attribute filter applied', () => {
|
||||
render(
|
||||
<AllAttributesValue
|
||||
filterKey="attribute1"
|
||||
@@ -198,10 +200,10 @@ describe('AllAttributesValue', () => {
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('value1'));
|
||||
fireEvent.click(screen.getByText('value1'));
|
||||
|
||||
expect(screen.getByText('Open in Explorer')).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByText('Open in Explorer'));
|
||||
fireEvent.click(screen.getByText('Open in Explorer'));
|
||||
expect(mockGoToMetricsExploreWithAppliedAttribute).toHaveBeenCalledWith(
|
||||
'attribute1',
|
||||
'value1',
|
||||
|
||||
@@ -1,18 +1,26 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { userEvent } from 'tests/test-utils';
|
||||
|
||||
import DashboardsAndAlertsPopover from '../DashboardsAndAlertsPopover';
|
||||
import {
|
||||
getMockAlertsData,
|
||||
getMockDashboardsData,
|
||||
MOCK_ALERT_1,
|
||||
MOCK_ALERT_2,
|
||||
MOCK_DASHBOARD_1,
|
||||
MOCK_DASHBOARD_2,
|
||||
MOCK_METRIC_NAME,
|
||||
} from './testUtlls';
|
||||
|
||||
const mockAlert1 = {
|
||||
alert_id: '1',
|
||||
alert_name: 'Alert 1',
|
||||
};
|
||||
const mockAlert2 = {
|
||||
alert_id: '2',
|
||||
alert_name: 'Alert 2',
|
||||
};
|
||||
const mockDashboard1 = {
|
||||
dashboard_id: '1',
|
||||
dashboard_name: 'Dashboard 1',
|
||||
};
|
||||
const mockDashboard2 = {
|
||||
dashboard_id: '2',
|
||||
dashboard_name: 'Dashboard 2',
|
||||
};
|
||||
const mockAlerts = [mockAlert1, mockAlert2];
|
||||
const mockDashboards = [mockDashboard1, mockDashboard2];
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
@@ -20,6 +28,7 @@ jest.mock('hooks/useSafeNavigate', () => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockSetQuery = jest.fn();
|
||||
const mockUrlQuery = {
|
||||
set: mockSetQuery,
|
||||
@@ -30,156 +39,125 @@ jest.mock('hooks/useUrlQuery', () => ({
|
||||
default: jest.fn(() => mockUrlQuery),
|
||||
}));
|
||||
|
||||
const useGetMetricAlertsMock = jest.spyOn(
|
||||
metricsExplorerHooks,
|
||||
'useGetMetricAlerts',
|
||||
);
|
||||
const useGetMetricDashboardsMock = jest.spyOn(
|
||||
metricsExplorerHooks,
|
||||
'useGetMetricDashboards',
|
||||
);
|
||||
|
||||
describe('DashboardsAndAlertsPopover', () => {
|
||||
beforeEach(() => {
|
||||
useGetMetricAlertsMock.mockReturnValue(getMockAlertsData());
|
||||
useGetMetricDashboardsMock.mockReturnValue(getMockDashboardsData());
|
||||
});
|
||||
|
||||
it('renders the popover correctly with multiple dashboards and alerts', () => {
|
||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||
render(
|
||||
<DashboardsAndAlertsPopover
|
||||
alerts={mockAlerts}
|
||||
dashboards={mockDashboards}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText(`2 dashboards`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`2 alert rules`)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(`${mockDashboards.length} dashboards`),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(`${mockAlerts.length} alert rules`),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders null with no dashboards and alerts', () => {
|
||||
useGetMetricAlertsMock.mockReturnValue(
|
||||
getMockAlertsData({
|
||||
data: {
|
||||
alerts: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
useGetMetricDashboardsMock.mockReturnValue(
|
||||
getMockDashboardsData({
|
||||
data: {
|
||||
dashboards: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const { container } = render(
|
||||
<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />,
|
||||
<DashboardsAndAlertsPopover alerts={[]} dashboards={[]} />,
|
||||
);
|
||||
expect(
|
||||
container.querySelector('dashboards-and-alerts-popover-container'),
|
||||
).toBeNull();
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders popover with single dashboard and alert', () => {
|
||||
useGetMetricAlertsMock.mockReturnValue(
|
||||
getMockAlertsData({
|
||||
data: {
|
||||
alerts: [MOCK_ALERT_1],
|
||||
},
|
||||
}),
|
||||
render(
|
||||
<DashboardsAndAlertsPopover
|
||||
alerts={[mockAlert1]}
|
||||
dashboards={[mockDashboard1]}
|
||||
/>,
|
||||
);
|
||||
useGetMetricDashboardsMock.mockReturnValue(
|
||||
getMockDashboardsData({
|
||||
data: {
|
||||
dashboards: [MOCK_DASHBOARD_1],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||
|
||||
expect(screen.getByText(`1 dashboard`)).toBeInTheDocument();
|
||||
expect(screen.getByText(`1 alert rule`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders popover with dashboard id if name is not available', async () => {
|
||||
useGetMetricDashboardsMock.mockReturnValue(
|
||||
getMockDashboardsData({
|
||||
data: {
|
||||
dashboards: [{ ...MOCK_DASHBOARD_1, dashboardName: '' }],
|
||||
},
|
||||
}),
|
||||
it('renders popover with dashboard id if name is not available', () => {
|
||||
render(
|
||||
<DashboardsAndAlertsPopover
|
||||
alerts={mockAlerts}
|
||||
dashboards={[{ ...mockDashboard1, dashboard_name: undefined } as any]}
|
||||
/>,
|
||||
);
|
||||
|
||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||
|
||||
await userEvent.click(screen.getByText(`1 dashboard`));
|
||||
expect(screen.getByText(MOCK_DASHBOARD_1.dashboardId)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText(`1 dashboard`));
|
||||
expect(screen.getByText(mockDashboard1.dashboard_id)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders popover with alert id if name is not available', async () => {
|
||||
useGetMetricAlertsMock.mockReturnValue(
|
||||
getMockAlertsData({
|
||||
data: {
|
||||
alerts: [{ ...MOCK_ALERT_1, alertName: '' }],
|
||||
},
|
||||
}),
|
||||
it('renders popover with alert id if name is not available', () => {
|
||||
render(
|
||||
<DashboardsAndAlertsPopover
|
||||
alerts={[{ ...mockAlert1, alert_name: undefined } as any]}
|
||||
dashboards={mockDashboards}
|
||||
/>,
|
||||
);
|
||||
|
||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||
|
||||
await userEvent.click(screen.getByText(`1 alert rule`));
|
||||
expect(screen.getByText(MOCK_ALERT_1.alertId)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText(`1 alert rule`));
|
||||
expect(screen.getByText(mockAlert1.alert_id)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('navigates to the dashboard when the dashboard is clicked', async () => {
|
||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||
it('navigates to the dashboard when the dashboard is clicked', () => {
|
||||
render(
|
||||
<DashboardsAndAlertsPopover
|
||||
alerts={mockAlerts}
|
||||
dashboards={mockDashboards}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Click on 2 dashboards button
|
||||
await userEvent.click(screen.getByText(`2 dashboards`));
|
||||
fireEvent.click(screen.getByText(`${mockDashboards.length} dashboards`));
|
||||
// Popover showing list of 2 dashboards should be visible
|
||||
expect(screen.getByText(MOCK_DASHBOARD_1.dashboardName)).toBeInTheDocument();
|
||||
expect(screen.getByText(MOCK_DASHBOARD_2.dashboardName)).toBeInTheDocument();
|
||||
expect(screen.getByText(mockDashboard1.dashboard_name)).toBeInTheDocument();
|
||||
expect(screen.getByText(mockDashboard2.dashboard_name)).toBeInTheDocument();
|
||||
|
||||
// Click on the first dashboard
|
||||
await userEvent.click(screen.getByText(MOCK_DASHBOARD_1.dashboardName));
|
||||
fireEvent.click(screen.getByText(mockDashboard1.dashboard_name));
|
||||
|
||||
// Should navigate to the dashboard
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
`/dashboard/${MOCK_DASHBOARD_1.dashboardId}`,
|
||||
`/dashboard/${mockDashboard1.dashboard_id}`,
|
||||
);
|
||||
});
|
||||
|
||||
it('navigates to the alert when the alert is clicked', async () => {
|
||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||
it('navigates to the alert when the alert is clicked', () => {
|
||||
render(
|
||||
<DashboardsAndAlertsPopover
|
||||
alerts={mockAlerts}
|
||||
dashboards={mockDashboards}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Click on 2 alert rules button
|
||||
await userEvent.click(screen.getByText(`2 alert rules`));
|
||||
fireEvent.click(screen.getByText(`${mockAlerts.length} alert rules`));
|
||||
// Popover showing list of 2 alert rules should be visible
|
||||
expect(screen.getByText(MOCK_ALERT_1.alertName)).toBeInTheDocument();
|
||||
expect(screen.getByText(MOCK_ALERT_2.alertName)).toBeInTheDocument();
|
||||
expect(screen.getByText(mockAlert1.alert_name)).toBeInTheDocument();
|
||||
expect(screen.getByText(mockAlert2.alert_name)).toBeInTheDocument();
|
||||
|
||||
// Click on the first alert rule
|
||||
await userEvent.click(screen.getByText(MOCK_ALERT_1.alertName));
|
||||
fireEvent.click(screen.getByText(mockAlert1.alert_name));
|
||||
|
||||
// Should navigate to the alert rule
|
||||
expect(mockSetQuery).toHaveBeenCalledWith(
|
||||
QueryParams.ruleId,
|
||||
MOCK_ALERT_1.alertId,
|
||||
mockAlert1.alert_id,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders unique dashboards even when there are duplicates', async () => {
|
||||
useGetMetricDashboardsMock.mockReturnValue(
|
||||
getMockDashboardsData({
|
||||
data: {
|
||||
dashboards: [MOCK_DASHBOARD_1, MOCK_DASHBOARD_2, MOCK_DASHBOARD_1],
|
||||
},
|
||||
}),
|
||||
it('renders unique dashboards even when there are duplicates', () => {
|
||||
render(
|
||||
<DashboardsAndAlertsPopover
|
||||
alerts={mockAlerts}
|
||||
dashboards={[...mockDashboards, mockDashboard1]}
|
||||
/>,
|
||||
);
|
||||
expect(
|
||||
screen.getByText(`${mockDashboards.length} dashboards`),
|
||||
).toBeInTheDocument();
|
||||
|
||||
render(<DashboardsAndAlertsPopover metricName={MOCK_METRIC_NAME} />);
|
||||
|
||||
expect(screen.getByText('2 dashboards')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByText('2 dashboards'));
|
||||
expect(screen.getByText(MOCK_DASHBOARD_1.dashboardName)).toBeInTheDocument();
|
||||
expect(screen.getByText(MOCK_DASHBOARD_2.dashboardName)).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText(`${mockDashboards.length} dashboards`));
|
||||
expect(screen.getByText(mockDashboard1.dashboard_name)).toBeInTheDocument();
|
||||
expect(screen.getByText(mockDashboard2.dashboard_name)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||
|
||||
import Highlights from '../Highlights';
|
||||
import { formatTimestampToReadableDate } from '../utils';
|
||||
import { getMockMetricHighlightsData, MOCK_METRIC_NAME } from './testUtlls';
|
||||
|
||||
const useGetMetricHighlightsMock = jest.spyOn(
|
||||
metricsExplorerHooks,
|
||||
'useGetMetricHighlights',
|
||||
);
|
||||
|
||||
describe('Highlights', () => {
|
||||
beforeEach(() => {
|
||||
useGetMetricHighlightsMock.mockReturnValue(getMockMetricHighlightsData());
|
||||
});
|
||||
|
||||
it('should render all highlights data correctly', () => {
|
||||
render(<Highlights metricName={MOCK_METRIC_NAME} />);
|
||||
|
||||
const dataPoints = screen.getByTestId('metric-highlights-data-points');
|
||||
const timeSeriesTotal = screen.getByTestId(
|
||||
'metric-highlights-time-series-total',
|
||||
);
|
||||
const lastReceived = screen.getByTestId('metric-highlights-last-received');
|
||||
|
||||
expect(dataPoints.textContent).toBe('1M+');
|
||||
expect(timeSeriesTotal.textContent).toBe('1M total ⎯ 1M active');
|
||||
expect(lastReceived.textContent).toBe(
|
||||
formatTimestampToReadableDate('2026-01-24T00:00:00Z'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should render error state correctly', () => {
|
||||
useGetMetricHighlightsMock.mockReturnValue(
|
||||
getMockMetricHighlightsData(
|
||||
{},
|
||||
{
|
||||
isError: true,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
render(<Highlights metricName={MOCK_METRIC_NAME} />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId('metric-highlights-error-state'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render loading state when data is loading', () => {
|
||||
useGetMetricHighlightsMock.mockReturnValue(
|
||||
getMockMetricHighlightsData(
|
||||
{},
|
||||
{
|
||||
isLoading: true,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
render(<Highlights metricName={MOCK_METRIC_NAME} />);
|
||||
|
||||
expect(
|
||||
screen.getByTestId('metric-highlights-loading-state'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,24 +1,16 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||
import {
|
||||
GetMetricMetadata200,
|
||||
MetrictypesTemporalityDTO,
|
||||
MetrictypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import {
|
||||
UniversalYAxisUnit,
|
||||
YAxisUnitSelectorProps,
|
||||
} from 'components/YAxisUnitSelector/types';
|
||||
import * as useUpdateMetricMetadataHooks from 'hooks/metricsExplorer/useUpdateMetricMetadata';
|
||||
import * as useNotificationsHooks from 'hooks/useNotifications';
|
||||
import { userEvent } from 'tests/test-utils';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
|
||||
import Metadata from '../Metadata';
|
||||
import { MetricMetadata } from '../types';
|
||||
import { transformMetricMetadata } from '../utils';
|
||||
import { getMockMetricMetadataData, MOCK_METRIC_NAME } from './testUtlls';
|
||||
|
||||
// Mock antd select for testing
|
||||
jest.mock('antd', () => ({
|
||||
@@ -80,18 +72,13 @@ jest.mock('react-query', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockUseUpdateMetricMetadataHook = jest.spyOn(
|
||||
metricsExplorerHooks,
|
||||
'useUpdateMetricMetadata',
|
||||
);
|
||||
type UseUpdateMetricMetadataResult = ReturnType<
|
||||
typeof metricsExplorerHooks.useUpdateMetricMetadata
|
||||
>;
|
||||
const mockUseUpdateMetricMetadata = jest.fn();
|
||||
|
||||
const mockMetricMetadata = transformMetricMetadata(
|
||||
getMockMetricMetadataData().data as GetMetricMetadata200,
|
||||
) as MetricMetadata;
|
||||
jest
|
||||
.spyOn(useUpdateMetricMetadataHooks, 'useUpdateMetricMetadata')
|
||||
.mockReturnValue({
|
||||
mutate: mockUseUpdateMetricMetadata,
|
||||
isLoading: false,
|
||||
} as any);
|
||||
|
||||
const mockErrorNotification = jest.fn();
|
||||
const mockSuccessNotification = jest.fn();
|
||||
@@ -102,50 +89,47 @@ jest.spyOn(useNotificationsHooks, 'useNotifications').mockReturnValue({
|
||||
},
|
||||
} as any);
|
||||
|
||||
const mockRefetchMetricMetadata = jest.fn();
|
||||
const mockMetricName = 'test_metric';
|
||||
const mockMetricMetadata = {
|
||||
metric_type: MetricType.GAUGE,
|
||||
description: 'test_description',
|
||||
unit: 'test_unit',
|
||||
temporality: Temporality.DELTA,
|
||||
};
|
||||
const mockRefetchMetricDetails = jest.fn();
|
||||
|
||||
describe('Metadata', () => {
|
||||
beforeEach(() => {
|
||||
mockUseUpdateMetricMetadataHook.mockReturnValue(({
|
||||
mutate: mockUseUpdateMetricMetadata,
|
||||
} as Partial<UseUpdateMetricMetadataResult>) as UseUpdateMetricMetadataResult);
|
||||
});
|
||||
|
||||
it('should render the metadata properly', () => {
|
||||
render(
|
||||
<Metadata
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricName={mockMetricName}
|
||||
metadata={mockMetricMetadata}
|
||||
isErrorMetricMetadata={false}
|
||||
isLoadingMetricMetadata={false}
|
||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||
refetchMetricDetails={mockRefetchMetricDetails}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Metric Type')).toBeInTheDocument();
|
||||
expect(screen.getByText('Gauge')).toBeInTheDocument();
|
||||
expect(screen.getByText(mockMetricMetadata.metric_type)).toBeInTheDocument();
|
||||
expect(screen.getByText('Description')).toBeInTheDocument();
|
||||
expect(screen.getByText(mockMetricMetadata.description)).toBeInTheDocument();
|
||||
expect(screen.getByText('Unit')).toBeInTheDocument();
|
||||
expect(screen.getByText(mockMetricMetadata.unit)).toBeInTheDocument();
|
||||
expect(screen.getByText('Temporality')).toBeInTheDocument();
|
||||
expect(screen.getByText('Delta')).toBeInTheDocument();
|
||||
expect(screen.getByText(mockMetricMetadata.temporality)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('editing the metadata should show the form inputs', async () => {
|
||||
it('editing the metadata should show the form inputs', () => {
|
||||
render(
|
||||
<Metadata
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricName={mockMetricName}
|
||||
metadata={mockMetricMetadata}
|
||||
isErrorMetricMetadata={false}
|
||||
isLoadingMetricMetadata={false}
|
||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||
refetchMetricDetails={mockRefetchMetricDetails}
|
||||
/>,
|
||||
);
|
||||
|
||||
const editButton = screen.getByText('Edit');
|
||||
expect(editButton).toBeInTheDocument();
|
||||
await userEvent.click(editButton);
|
||||
fireEvent.click(editButton);
|
||||
|
||||
expect(screen.getByTestId('metric-type-select')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('temporality-select')).toBeInTheDocument();
|
||||
@@ -155,53 +139,57 @@ describe('Metadata', () => {
|
||||
it('should update the metadata when the form is submitted', async () => {
|
||||
render(
|
||||
<Metadata
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricName={mockMetricName}
|
||||
metadata={{
|
||||
...mockMetricMetadata,
|
||||
unit: '',
|
||||
}}
|
||||
isErrorMetricMetadata={false}
|
||||
isLoadingMetricMetadata={false}
|
||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||
refetchMetricDetails={mockRefetchMetricDetails}
|
||||
/>,
|
||||
);
|
||||
|
||||
const editButton = screen.getByText('Edit');
|
||||
expect(editButton).toBeInTheDocument();
|
||||
await userEvent.click(editButton);
|
||||
fireEvent.click(editButton);
|
||||
|
||||
const metricDescriptionInput = screen.getByTestId('description-input');
|
||||
expect(metricDescriptionInput).toBeInTheDocument();
|
||||
await userEvent.clear(metricDescriptionInput);
|
||||
await userEvent.type(metricDescriptionInput, 'Updated description');
|
||||
fireEvent.change(metricDescriptionInput, {
|
||||
target: { value: 'Updated description' },
|
||||
});
|
||||
|
||||
const metricTypeSelect = screen.getByTestId('metric-type-select');
|
||||
expect(metricTypeSelect).toBeInTheDocument();
|
||||
await userEvent.selectOptions(metricTypeSelect, MetrictypesTypeDTO.sum);
|
||||
fireEvent.change(metricTypeSelect, {
|
||||
target: { value: MetricType.SUM },
|
||||
});
|
||||
|
||||
const temporalitySelect = screen.getByTestId('temporality-select');
|
||||
expect(temporalitySelect).toBeInTheDocument();
|
||||
await userEvent.selectOptions(temporalitySelect, Temporality.CUMULATIVE);
|
||||
fireEvent.change(temporalitySelect, {
|
||||
target: { value: Temporality.CUMULATIVE },
|
||||
});
|
||||
|
||||
const unitSelect = screen.getByTestId('unit-select');
|
||||
expect(unitSelect).toBeInTheDocument();
|
||||
await userEvent.selectOptions(unitSelect, 'By');
|
||||
fireEvent.change(unitSelect, {
|
||||
target: { value: 'By' },
|
||||
});
|
||||
|
||||
const saveButton = screen.getByText('Save');
|
||||
expect(saveButton).toBeInTheDocument();
|
||||
await userEvent.click(saveButton);
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
expect(mockUseUpdateMetricMetadata).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
type: MetrictypesTypeDTO.sum,
|
||||
temporality: MetrictypesTemporalityDTO.cumulative,
|
||||
metricName: mockMetricName,
|
||||
payload: expect.objectContaining({
|
||||
description: 'Updated description',
|
||||
metricType: MetricType.SUM,
|
||||
temporality: Temporality.CUMULATIVE,
|
||||
unit: 'By',
|
||||
isMonotonic: true,
|
||||
}),
|
||||
pathParams: {
|
||||
metricName: MOCK_METRIC_NAME,
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
@@ -213,56 +201,56 @@ describe('Metadata', () => {
|
||||
it('should show success notification when metadata is updated successfully', async () => {
|
||||
render(
|
||||
<Metadata
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricName={mockMetricName}
|
||||
metadata={mockMetricMetadata}
|
||||
isErrorMetricMetadata={false}
|
||||
isLoadingMetricMetadata={false}
|
||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||
refetchMetricDetails={mockRefetchMetricDetails}
|
||||
/>,
|
||||
);
|
||||
|
||||
const editButton = screen.getByText('Edit');
|
||||
await userEvent.click(editButton);
|
||||
fireEvent.click(editButton);
|
||||
|
||||
const metricDescriptionInput = screen.getByTestId('description-input');
|
||||
await userEvent.clear(metricDescriptionInput);
|
||||
await userEvent.type(metricDescriptionInput, 'Updated description');
|
||||
fireEvent.change(metricDescriptionInput, {
|
||||
target: { value: 'Updated description' },
|
||||
});
|
||||
|
||||
const saveButton = screen.getByText('Save');
|
||||
await userEvent.click(saveButton);
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
const onSuccessCallback =
|
||||
mockUseUpdateMetricMetadata.mock.calls[0][1].onSuccess;
|
||||
onSuccessCallback({ status: 200 });
|
||||
onSuccessCallback({ statusCode: 200 });
|
||||
|
||||
expect(mockSuccessNotification).toHaveBeenCalledWith({
|
||||
message: 'Metadata updated successfully',
|
||||
});
|
||||
expect(mockRefetchMetricDetails).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show error notification when metadata update fails with non-200 response', async () => {
|
||||
render(
|
||||
<Metadata
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricName={mockMetricName}
|
||||
metadata={mockMetricMetadata}
|
||||
isErrorMetricMetadata={false}
|
||||
isLoadingMetricMetadata={false}
|
||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||
refetchMetricDetails={mockRefetchMetricDetails}
|
||||
/>,
|
||||
);
|
||||
|
||||
const editButton = screen.getByText('Edit');
|
||||
await userEvent.click(editButton);
|
||||
fireEvent.click(editButton);
|
||||
|
||||
const metricDescriptionInput = screen.getByTestId('description-input');
|
||||
await userEvent.clear(metricDescriptionInput);
|
||||
await userEvent.type(metricDescriptionInput, 'Updated description');
|
||||
fireEvent.change(metricDescriptionInput, {
|
||||
target: { value: 'Updated description' },
|
||||
});
|
||||
|
||||
const saveButton = screen.getByText('Save');
|
||||
await userEvent.click(saveButton);
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
const onErrorCallback = mockUseUpdateMetricMetadata.mock.calls[0][1].onError;
|
||||
onErrorCallback({ status: 500 });
|
||||
const onSuccessCallback =
|
||||
mockUseUpdateMetricMetadata.mock.calls[0][1].onSuccess;
|
||||
onSuccessCallback({ statusCode: 500 });
|
||||
|
||||
expect(mockErrorNotification).toHaveBeenCalledWith({
|
||||
message:
|
||||
@@ -273,23 +261,22 @@ describe('Metadata', () => {
|
||||
it('should show error notification when metadata update fails', async () => {
|
||||
render(
|
||||
<Metadata
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricName={mockMetricName}
|
||||
metadata={mockMetricMetadata}
|
||||
isErrorMetricMetadata={false}
|
||||
isLoadingMetricMetadata={false}
|
||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||
refetchMetricDetails={mockRefetchMetricDetails}
|
||||
/>,
|
||||
);
|
||||
|
||||
const editButton = screen.getByText('Edit');
|
||||
await userEvent.click(editButton);
|
||||
fireEvent.click(editButton);
|
||||
|
||||
const metricDescriptionInput = screen.getByTestId('description-input');
|
||||
await userEvent.clear(metricDescriptionInput);
|
||||
await userEvent.type(metricDescriptionInput, 'Updated description');
|
||||
fireEvent.change(metricDescriptionInput, {
|
||||
target: { value: 'Updated description' },
|
||||
});
|
||||
|
||||
const saveButton = screen.getByText('Save');
|
||||
await userEvent.click(saveButton);
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
const onErrorCallback = mockUseUpdateMetricMetadata.mock.calls[0][1].onError;
|
||||
|
||||
@@ -302,43 +289,39 @@ describe('Metadata', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('cancel button should cancel the edit mode', async () => {
|
||||
it('cancel button should cancel the edit mode', () => {
|
||||
render(
|
||||
<Metadata
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricName={mockMetricName}
|
||||
metadata={mockMetricMetadata}
|
||||
isErrorMetricMetadata={false}
|
||||
isLoadingMetricMetadata={false}
|
||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||
refetchMetricDetails={mockRefetchMetricDetails}
|
||||
/>,
|
||||
);
|
||||
|
||||
const editButton = screen.getByText('Edit');
|
||||
expect(editButton).toBeInTheDocument();
|
||||
await userEvent.click(editButton);
|
||||
fireEvent.click(editButton);
|
||||
|
||||
const cancelButton = screen.getByText('Cancel');
|
||||
expect(cancelButton).toBeInTheDocument();
|
||||
await userEvent.click(cancelButton);
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
const editButton2 = screen.getByText('Edit');
|
||||
expect(editButton2).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not allow editing of unit if it is already set', async () => {
|
||||
it('should not allow editing of unit if it is already set', () => {
|
||||
render(
|
||||
<Metadata
|
||||
metricName={MOCK_METRIC_NAME}
|
||||
metricName={mockMetricName}
|
||||
metadata={mockMetricMetadata}
|
||||
isErrorMetricMetadata={false}
|
||||
isLoadingMetricMetadata={false}
|
||||
refetchMetricMetadata={mockRefetchMetricMetadata}
|
||||
refetchMetricDetails={mockRefetchMetricDetails}
|
||||
/>,
|
||||
);
|
||||
|
||||
const editButton = screen.getByText('Edit');
|
||||
expect(editButton).toBeInTheDocument();
|
||||
await userEvent.click(editButton);
|
||||
fireEvent.click(editButton);
|
||||
|
||||
const unitSelect = screen.queryByTestId('unit-select');
|
||||
expect(unitSelect).not.toBeInTheDocument();
|
||||
|
||||
@@ -1,16 +1,68 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { MetricDetails as MetricDetailsType } from 'api/metricsExplorer/getMetricDetails';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import { getUniversalNameFromMetricUnit } from 'components/YAxisUnitSelector/utils';
|
||||
import ROUTES from 'constants/routes';
|
||||
import * as useGetMetricDetails from 'hooks/metricsExplorer/useGetMetricDetails';
|
||||
import * as useUpdateMetricMetadata from 'hooks/metricsExplorer/useUpdateMetricMetadata';
|
||||
import * as useHandleExplorerTabChange from 'hooks/useHandleExplorerTabChange';
|
||||
import { userEvent } from 'tests/test-utils';
|
||||
|
||||
import MetricDetails from '../MetricDetails';
|
||||
import { getMockMetricMetadataData } from './testUtlls';
|
||||
|
||||
const mockMetricName = 'test-metric';
|
||||
const mockMetricDescription = 'description for a test metric';
|
||||
const mockMetricData: MetricDetailsType = {
|
||||
name: mockMetricName,
|
||||
description: mockMetricDescription,
|
||||
unit: 'count',
|
||||
attributes: [
|
||||
{
|
||||
key: 'test-attribute',
|
||||
value: ['test-value'],
|
||||
valueCount: 1,
|
||||
},
|
||||
],
|
||||
alerts: [],
|
||||
dashboards: [],
|
||||
metadata: {
|
||||
metric_type: MetricType.SUM,
|
||||
description: mockMetricDescription,
|
||||
unit: 'count',
|
||||
},
|
||||
type: '',
|
||||
timeseries: 0,
|
||||
samples: 0,
|
||||
timeSeriesTotal: 0,
|
||||
timeSeriesActive: 0,
|
||||
lastReceived: '',
|
||||
};
|
||||
const mockOpenInspectModal = jest.fn();
|
||||
const mockOnClose = jest.fn();
|
||||
|
||||
const mockUseGetMetricDetailsData = {
|
||||
data: {
|
||||
payload: {
|
||||
data: mockMetricData,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
refetch: jest.fn(),
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(useGetMetricDetails, 'useGetMetricDetails')
|
||||
.mockReturnValue(mockUseGetMetricDetailsData as any);
|
||||
|
||||
jest.spyOn(useUpdateMetricMetadata, 'useUpdateMetricMetadata').mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
error: null,
|
||||
} as any);
|
||||
|
||||
const mockHandleExplorerTabChange = jest.fn();
|
||||
jest
|
||||
.spyOn(useHandleExplorerTabChange, 'useHandleExplorerTabChange')
|
||||
@@ -36,50 +88,7 @@ jest.mock('react-query', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/MetricsExplorer/MetricDetails/AllAttributes',
|
||||
() =>
|
||||
function MockAllAttributes(): JSX.Element {
|
||||
return <div data-testid="all-attributes">All Attributes</div>;
|
||||
},
|
||||
);
|
||||
jest.mock(
|
||||
'container/MetricsExplorer/MetricDetails/DashboardsAndAlertsPopover',
|
||||
() =>
|
||||
function MockDashboardsAndAlertsPopover(): JSX.Element {
|
||||
return (
|
||||
<div data-testid="dashboards-and-alerts-popover">
|
||||
Dashboards and Alerts Popover
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
jest.mock(
|
||||
'container/MetricsExplorer/MetricDetails/Highlights',
|
||||
() =>
|
||||
function MockHighlights(): JSX.Element {
|
||||
return <div data-testid="highlights">Highlights</div>;
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock(
|
||||
'container/MetricsExplorer/MetricDetails/Metadata',
|
||||
() =>
|
||||
function MockMetadata(): JSX.Element {
|
||||
return <div data-testid="metadata">Metadata</div>;
|
||||
},
|
||||
);
|
||||
|
||||
const useGetMetricMetadataMock = jest.spyOn(
|
||||
metricsExplorerHooks,
|
||||
'useGetMetricMetadata',
|
||||
);
|
||||
|
||||
describe('MetricDetails', () => {
|
||||
beforeEach(() => {
|
||||
useGetMetricMetadataMock.mockReturnValue(getMockMetricMetadataData());
|
||||
});
|
||||
|
||||
it('renders metric details correctly', () => {
|
||||
render(
|
||||
<MetricDetails
|
||||
@@ -92,15 +101,27 @@ describe('MetricDetails', () => {
|
||||
);
|
||||
|
||||
expect(screen.getByText(mockMetricName)).toBeInTheDocument();
|
||||
expect(screen.getByTestId('all-attributes')).toBeInTheDocument();
|
||||
expect(screen.getByText(mockMetricDescription)).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByTestId('dashboards-and-alerts-popover'),
|
||||
screen.getByText(getUniversalNameFromMetricUnit(mockMetricData.unit)),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('highlights')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('metadata')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the "open in explorer" and "inspect" buttons', async () => {
|
||||
it('renders the "open in explorer" and "inspect" buttons', () => {
|
||||
jest.spyOn(useGetMetricDetails, 'useGetMetricDetails').mockReturnValueOnce({
|
||||
...mockUseGetMetricDetailsData,
|
||||
data: {
|
||||
payload: {
|
||||
data: {
|
||||
...mockMetricData,
|
||||
metadata: {
|
||||
...mockMetricData.metadata,
|
||||
metric_type: MetricType.GAUGE,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
render(
|
||||
<MetricDetails
|
||||
onClose={mockOnClose}
|
||||
@@ -114,10 +135,93 @@ describe('MetricDetails', () => {
|
||||
expect(screen.getByTestId('open-in-explorer-button')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('inspect-metric-button')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(screen.getByTestId('open-in-explorer-button'));
|
||||
fireEvent.click(screen.getByTestId('open-in-explorer-button'));
|
||||
expect(mockHandleExplorerTabChange).toHaveBeenCalled();
|
||||
|
||||
await userEvent.click(screen.getByTestId('inspect-metric-button'));
|
||||
fireEvent.click(screen.getByTestId('inspect-metric-button'));
|
||||
expect(mockOpenInspectModal).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should render error state when metric details are not found', () => {
|
||||
jest.spyOn(useGetMetricDetails, 'useGetMetricDetails').mockReturnValue({
|
||||
...mockUseGetMetricDetailsData,
|
||||
isError: true,
|
||||
error: {
|
||||
message: 'Error fetching metric details',
|
||||
},
|
||||
} as any);
|
||||
|
||||
render(
|
||||
<MetricDetails
|
||||
onClose={mockOnClose}
|
||||
isOpen
|
||||
metricName={mockMetricName}
|
||||
isModalTimeSelection
|
||||
openInspectModal={mockOpenInspectModal}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Error fetching metric details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render loading state when metric details are loading', () => {
|
||||
jest.spyOn(useGetMetricDetails, 'useGetMetricDetails').mockReturnValue({
|
||||
...mockUseGetMetricDetailsData,
|
||||
isLoading: true,
|
||||
} as any);
|
||||
|
||||
render(
|
||||
<MetricDetails
|
||||
onClose={mockOnClose}
|
||||
isOpen
|
||||
metricName={mockMetricName}
|
||||
isModalTimeSelection
|
||||
openInspectModal={mockOpenInspectModal}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('metric-details-skeleton')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render all attributes section', () => {
|
||||
jest
|
||||
.spyOn(useGetMetricDetails, 'useGetMetricDetails')
|
||||
.mockReturnValue(mockUseGetMetricDetailsData as any);
|
||||
render(
|
||||
<MetricDetails
|
||||
onClose={mockOnClose}
|
||||
isOpen
|
||||
metricName={mockMetricName}
|
||||
isModalTimeSelection
|
||||
openInspectModal={mockOpenInspectModal}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('All Attributes')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render all attributes section when relevant data is not present', () => {
|
||||
jest.spyOn(useGetMetricDetails, 'useGetMetricDetails').mockReturnValue({
|
||||
...mockUseGetMetricDetailsData,
|
||||
data: {
|
||||
payload: {
|
||||
data: {
|
||||
...mockMetricData,
|
||||
attributes: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
render(
|
||||
<MetricDetails
|
||||
onClose={mockOnClose}
|
||||
isOpen
|
||||
metricName={mockMetricName}
|
||||
isModalTimeSelection
|
||||
openInspectModal={mockOpenInspectModal}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByText('All Attributes')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
import * as metricsExplorerHooks from 'api/generated/services/metrics';
|
||||
import {
|
||||
GetMetricAlerts200,
|
||||
GetMetricAttributes200,
|
||||
GetMetricDashboards200,
|
||||
GetMetricHighlights200,
|
||||
GetMetricMetadata200,
|
||||
MetrictypesTemporalityDTO,
|
||||
MetrictypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export const MOCK_METRIC_NAME = 'test-metric';
|
||||
|
||||
export function getMockMetricHighlightsData(
|
||||
overrides?: Partial<GetMetricHighlights200>,
|
||||
{
|
||||
isLoading = false,
|
||||
isError = false,
|
||||
}: {
|
||||
isLoading?: boolean;
|
||||
isError?: boolean;
|
||||
} = {},
|
||||
): ReturnType<typeof metricsExplorerHooks.useGetMetricHighlights> {
|
||||
return {
|
||||
data: {
|
||||
data: {
|
||||
dataPoints: 1000000,
|
||||
lastReceived: '2026-01-24T00:00:00Z',
|
||||
totalTimeSeries: 1000000,
|
||||
activeTimeSeries: 1000000,
|
||||
},
|
||||
status: 'success',
|
||||
...overrides,
|
||||
},
|
||||
isLoading,
|
||||
isError,
|
||||
} as ReturnType<typeof metricsExplorerHooks.useGetMetricHighlights>;
|
||||
}
|
||||
|
||||
export const MOCK_DASHBOARD_1 = {
|
||||
dashboardName: 'Dashboard 1',
|
||||
dashboardId: '1',
|
||||
widgetId: '1',
|
||||
widgetName: 'Widget 1',
|
||||
};
|
||||
export const MOCK_DASHBOARD_2 = {
|
||||
dashboardName: 'Dashboard 2',
|
||||
dashboardId: '2',
|
||||
widgetId: '2',
|
||||
widgetName: 'Widget 2',
|
||||
};
|
||||
export const MOCK_ALERT_1 = {
|
||||
alertName: 'Alert 1',
|
||||
alertId: '1',
|
||||
};
|
||||
export const MOCK_ALERT_2 = {
|
||||
alertName: 'Alert 2',
|
||||
alertId: '2',
|
||||
};
|
||||
|
||||
export function getMockDashboardsData(
|
||||
overrides?: Partial<GetMetricDashboards200>,
|
||||
{
|
||||
isLoading = false,
|
||||
isError = false,
|
||||
}: {
|
||||
isLoading?: boolean;
|
||||
isError?: boolean;
|
||||
} = {},
|
||||
): ReturnType<typeof metricsExplorerHooks.useGetMetricDashboards> {
|
||||
return {
|
||||
data: {
|
||||
data: {
|
||||
dashboards: [MOCK_DASHBOARD_1, MOCK_DASHBOARD_2],
|
||||
},
|
||||
status: 'success',
|
||||
...overrides,
|
||||
},
|
||||
|
||||
isLoading,
|
||||
isError,
|
||||
} as ReturnType<typeof metricsExplorerHooks.useGetMetricDashboards>;
|
||||
}
|
||||
|
||||
export function getMockAlertsData(
|
||||
overrides?: Partial<GetMetricAlerts200>,
|
||||
{
|
||||
isLoading = false,
|
||||
isError = false,
|
||||
}: {
|
||||
isLoading?: boolean;
|
||||
isError?: boolean;
|
||||
} = {},
|
||||
): ReturnType<typeof metricsExplorerHooks.useGetMetricAlerts> {
|
||||
return {
|
||||
data: {
|
||||
data: {
|
||||
alerts: [MOCK_ALERT_1, MOCK_ALERT_2],
|
||||
},
|
||||
status: 'success',
|
||||
...overrides,
|
||||
},
|
||||
isLoading,
|
||||
isError,
|
||||
} as ReturnType<typeof metricsExplorerHooks.useGetMetricAlerts>;
|
||||
}
|
||||
|
||||
export function getMockMetricAttributesData(
|
||||
overrides?: Partial<GetMetricAttributes200>,
|
||||
{
|
||||
isLoading = false,
|
||||
isError = false,
|
||||
}: {
|
||||
isLoading?: boolean;
|
||||
isError?: boolean;
|
||||
} = {},
|
||||
): ReturnType<typeof metricsExplorerHooks.useGetMetricAttributes> {
|
||||
return {
|
||||
data: {
|
||||
data: {
|
||||
attributes: [
|
||||
{
|
||||
key: 'attribute1',
|
||||
values: ['value1', 'value2'],
|
||||
valueCount: 2,
|
||||
},
|
||||
{
|
||||
key: 'attribute2',
|
||||
values: ['value3'],
|
||||
valueCount: 1,
|
||||
},
|
||||
],
|
||||
totalKeys: 2,
|
||||
},
|
||||
status: 'success',
|
||||
...overrides,
|
||||
},
|
||||
isLoading,
|
||||
isError,
|
||||
} as ReturnType<typeof metricsExplorerHooks.useGetMetricAttributes>;
|
||||
}
|
||||
|
||||
export function getMockMetricMetadataData(
|
||||
overrides?: Partial<GetMetricMetadata200>,
|
||||
{
|
||||
isLoading = false,
|
||||
isError = false,
|
||||
}: {
|
||||
isLoading?: boolean;
|
||||
isError?: boolean;
|
||||
} = {},
|
||||
): ReturnType<typeof metricsExplorerHooks.useGetMetricMetadata> {
|
||||
return {
|
||||
data: {
|
||||
data: {
|
||||
description: 'test_description',
|
||||
type: MetrictypesTypeDTO.gauge,
|
||||
unit: 'test_unit',
|
||||
temporality: MetrictypesTemporalityDTO.delta,
|
||||
isMonotonic: false,
|
||||
},
|
||||
status: 'success',
|
||||
...overrides,
|
||||
},
|
||||
isLoading,
|
||||
isError,
|
||||
} as ReturnType<typeof metricsExplorerHooks.useGetMetricMetadata>;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
MetrictypesTemporalityDTO,
|
||||
MetrictypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
|
||||
import {
|
||||
determineIsMonotonic,
|
||||
@@ -12,48 +10,35 @@ import {
|
||||
describe('MetricDetails utils', () => {
|
||||
describe('determineIsMonotonic', () => {
|
||||
it('should return true for histogram metrics', () => {
|
||||
expect(determineIsMonotonic(MetrictypesTypeDTO.histogram)).toBe(true);
|
||||
expect(determineIsMonotonic(MetricType.HISTOGRAM)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for exponential histogram metrics', () => {
|
||||
expect(determineIsMonotonic(MetrictypesTypeDTO.exponentialhistogram)).toBe(
|
||||
expect(determineIsMonotonic(MetricType.EXPONENTIAL_HISTOGRAM)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for gauge metrics', () => {
|
||||
expect(determineIsMonotonic(MetricType.GAUGE)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for summary metrics', () => {
|
||||
expect(determineIsMonotonic(MetricType.SUMMARY)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for sum metrics with cumulative temporality', () => {
|
||||
expect(determineIsMonotonic(MetricType.SUM, Temporality.CUMULATIVE)).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return false for gauge metrics', () => {
|
||||
expect(determineIsMonotonic(MetrictypesTypeDTO.gauge)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for summary metrics', () => {
|
||||
expect(determineIsMonotonic(MetrictypesTypeDTO.summary)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for sum metrics with cumulative temporality', () => {
|
||||
expect(
|
||||
determineIsMonotonic(
|
||||
MetrictypesTypeDTO.sum,
|
||||
MetrictypesTemporalityDTO.cumulative,
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for sum metrics with delta temporality', () => {
|
||||
expect(
|
||||
determineIsMonotonic(
|
||||
MetrictypesTypeDTO.sum,
|
||||
MetrictypesTemporalityDTO.delta,
|
||||
),
|
||||
).toBe(false);
|
||||
expect(determineIsMonotonic(MetricType.SUM, Temporality.DELTA)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false by default', () => {
|
||||
expect(
|
||||
determineIsMonotonic(
|
||||
'' as MetrictypesTypeDTO,
|
||||
'' as MetrictypesTemporalityDTO,
|
||||
),
|
||||
).toBe(false);
|
||||
expect(determineIsMonotonic('' as MetricType, '' as Temporality)).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -130,16 +115,13 @@ describe('MetricDetails utils', () => {
|
||||
const API_GATEWAY = 'api-gateway';
|
||||
|
||||
it('should create correct query for SUM metric type', () => {
|
||||
const query = getMetricDetailsQuery(
|
||||
TEST_METRIC_NAME,
|
||||
MetrictypesTypeDTO.sum,
|
||||
);
|
||||
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.SUM);
|
||||
|
||||
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
||||
TEST_METRIC_NAME,
|
||||
);
|
||||
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
||||
MetrictypesTypeDTO.sum,
|
||||
MetricType.SUM,
|
||||
);
|
||||
expect(query.builder.queryData[0]?.aggregateOperator).toBe('rate');
|
||||
expect(query.builder.queryData[0]?.timeAggregation).toBe('rate');
|
||||
@@ -147,16 +129,13 @@ describe('MetricDetails utils', () => {
|
||||
});
|
||||
|
||||
it('should create correct query for GAUGE metric type', () => {
|
||||
const query = getMetricDetailsQuery(
|
||||
TEST_METRIC_NAME,
|
||||
MetrictypesTypeDTO.gauge,
|
||||
);
|
||||
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.GAUGE);
|
||||
|
||||
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
||||
TEST_METRIC_NAME,
|
||||
);
|
||||
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
||||
MetrictypesTypeDTO.gauge,
|
||||
MetricType.GAUGE,
|
||||
);
|
||||
expect(query.builder.queryData[0]?.aggregateOperator).toBe('avg');
|
||||
expect(query.builder.queryData[0]?.timeAggregation).toBe('avg');
|
||||
@@ -164,16 +143,13 @@ describe('MetricDetails utils', () => {
|
||||
});
|
||||
|
||||
it('should create correct query for SUMMARY metric type', () => {
|
||||
const query = getMetricDetailsQuery(
|
||||
TEST_METRIC_NAME,
|
||||
MetrictypesTypeDTO.summary,
|
||||
);
|
||||
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.SUMMARY);
|
||||
|
||||
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
||||
TEST_METRIC_NAME,
|
||||
);
|
||||
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
||||
MetrictypesTypeDTO.summary,
|
||||
MetricType.SUMMARY,
|
||||
);
|
||||
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
|
||||
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
|
||||
@@ -181,16 +157,13 @@ describe('MetricDetails utils', () => {
|
||||
});
|
||||
|
||||
it('should create correct query for HISTOGRAM metric type', () => {
|
||||
const query = getMetricDetailsQuery(
|
||||
TEST_METRIC_NAME,
|
||||
MetrictypesTypeDTO.histogram,
|
||||
);
|
||||
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.HISTOGRAM);
|
||||
|
||||
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
||||
TEST_METRIC_NAME,
|
||||
);
|
||||
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
||||
MetrictypesTypeDTO.histogram,
|
||||
MetricType.HISTOGRAM,
|
||||
);
|
||||
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
|
||||
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
|
||||
@@ -200,14 +173,14 @@ describe('MetricDetails utils', () => {
|
||||
it('should create correct query for EXPONENTIAL_HISTOGRAM metric type', () => {
|
||||
const query = getMetricDetailsQuery(
|
||||
TEST_METRIC_NAME,
|
||||
MetrictypesTypeDTO.exponentialhistogram,
|
||||
MetricType.EXPONENTIAL_HISTOGRAM,
|
||||
);
|
||||
|
||||
expect(query.builder.queryData[0]?.aggregateAttribute?.key).toBe(
|
||||
TEST_METRIC_NAME,
|
||||
);
|
||||
expect(query.builder.queryData[0]?.aggregateAttribute?.type).toBe(
|
||||
MetrictypesTypeDTO.exponentialhistogram,
|
||||
MetricType.EXPONENTIAL_HISTOGRAM,
|
||||
);
|
||||
expect(query.builder.queryData[0]?.aggregateOperator).toBe('noop');
|
||||
expect(query.builder.queryData[0]?.timeAggregation).toBe('noop');
|
||||
@@ -230,7 +203,7 @@ describe('MetricDetails utils', () => {
|
||||
const filter = { key: 'service', value: API_GATEWAY };
|
||||
const query = getMetricDetailsQuery(
|
||||
TEST_METRIC_NAME,
|
||||
MetrictypesTypeDTO.sum,
|
||||
MetricType.SUM,
|
||||
filter,
|
||||
);
|
||||
|
||||
@@ -248,7 +221,7 @@ describe('MetricDetails utils', () => {
|
||||
const groupBy = 'service';
|
||||
const query = getMetricDetailsQuery(
|
||||
TEST_METRIC_NAME,
|
||||
MetrictypesTypeDTO.sum,
|
||||
MetricType.SUM,
|
||||
undefined,
|
||||
groupBy,
|
||||
);
|
||||
@@ -263,7 +236,7 @@ describe('MetricDetails utils', () => {
|
||||
const groupBy = 'endpoint';
|
||||
const query = getMetricDetailsQuery(
|
||||
TEST_METRIC_NAME,
|
||||
MetrictypesTypeDTO.sum,
|
||||
MetricType.SUM,
|
||||
filter,
|
||||
groupBy,
|
||||
);
|
||||
@@ -277,10 +250,7 @@ describe('MetricDetails utils', () => {
|
||||
});
|
||||
|
||||
it('should not include filters or groupBy when not provided', () => {
|
||||
const query = getMetricDetailsQuery(
|
||||
TEST_METRIC_NAME,
|
||||
MetrictypesTypeDTO.sum,
|
||||
);
|
||||
const query = getMetricDetailsQuery(TEST_METRIC_NAME, MetricType.SUM);
|
||||
|
||||
expect(query.builder.queryData[0]?.filters?.items).toHaveLength(0);
|
||||
expect(query.builder.queryData[0]?.groupBy).toHaveLength(0);
|
||||
|
||||
@@ -1,55 +1,6 @@
|
||||
import {
|
||||
MetrictypesTemporalityDTO,
|
||||
MetrictypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
|
||||
export const METRIC_METADATA_KEYS = {
|
||||
description: 'Description',
|
||||
unit: 'Unit',
|
||||
type: 'Metric Type',
|
||||
metric_type: 'Metric Type',
|
||||
temporality: 'Temporality',
|
||||
isMonotonic: 'Monotonic',
|
||||
};
|
||||
|
||||
export const METRIC_METADATA_TEMPORALITY_OPTIONS: Array<{
|
||||
value: MetrictypesTemporalityDTO;
|
||||
label: string;
|
||||
}> = [
|
||||
{
|
||||
value: MetrictypesTemporalityDTO.delta,
|
||||
label: 'Delta',
|
||||
},
|
||||
{
|
||||
value: MetrictypesTemporalityDTO.cumulative,
|
||||
label: 'Cumulative',
|
||||
},
|
||||
];
|
||||
|
||||
export const METRIC_METADATA_TYPE_OPTIONS: Array<{
|
||||
value: MetrictypesTypeDTO;
|
||||
label: string;
|
||||
}> = [
|
||||
{
|
||||
value: MetrictypesTypeDTO.sum,
|
||||
label: 'Sum',
|
||||
},
|
||||
{
|
||||
value: MetrictypesTypeDTO.gauge,
|
||||
label: 'Gauge',
|
||||
},
|
||||
{
|
||||
value: MetrictypesTypeDTO.histogram,
|
||||
label: 'Histogram',
|
||||
},
|
||||
{
|
||||
value: MetrictypesTypeDTO.summary,
|
||||
label: 'Summary',
|
||||
},
|
||||
{
|
||||
value: MetrictypesTypeDTO.exponentialhistogram,
|
||||
label: 'Exponential Histogram',
|
||||
},
|
||||
];
|
||||
|
||||
export const METRIC_METADATA_UPDATE_ERROR_MESSAGE =
|
||||
'Failed to update metadata, please try again. If the issue persists, please contact support.';
|
||||
|
||||
@@ -1,39 +1,34 @@
|
||||
import {
|
||||
MetricsexplorertypesMetricAlertDTO,
|
||||
MetricsexplorertypesMetricAttributeDTO,
|
||||
MetricsexplorertypesMetricDashboardDTO,
|
||||
MetricsexplorertypesMetricHighlightsResponseDTO,
|
||||
MetricsexplorertypesMetricMetadataDTO,
|
||||
MetrictypesTemporalityDTO,
|
||||
MetrictypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
MetricDetails,
|
||||
MetricDetailsAlert,
|
||||
MetricDetailsAttribute,
|
||||
MetricDetailsDashboard,
|
||||
} from 'api/metricsExplorer/getMetricDetails';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
|
||||
export interface MetricDetailsProps {
|
||||
onClose: () => void;
|
||||
isOpen: boolean;
|
||||
metricName: string;
|
||||
metricName: string | null;
|
||||
isModalTimeSelection: boolean;
|
||||
openInspectModal?: (metricName: string) => void;
|
||||
}
|
||||
|
||||
export interface HighlightsProps {
|
||||
metricName: string;
|
||||
}
|
||||
export interface DashboardsAndAlertsPopoverProps {
|
||||
metricName: string;
|
||||
dashboards: MetricDetailsDashboard[] | null;
|
||||
alerts: MetricDetailsAlert[] | null;
|
||||
}
|
||||
|
||||
export interface MetadataProps {
|
||||
metricName: string;
|
||||
metadata: MetricMetadata | null;
|
||||
isErrorMetricMetadata: boolean;
|
||||
isLoadingMetricMetadata: boolean;
|
||||
refetchMetricMetadata: () => void;
|
||||
metadata: MetricDetails['metadata'] | undefined;
|
||||
refetchMetricDetails: () => void;
|
||||
}
|
||||
|
||||
export interface AllAttributesProps {
|
||||
attributes: MetricDetailsAttribute[];
|
||||
metricName: string;
|
||||
metricType: MetrictypesTypeDTO | undefined;
|
||||
metricType: MetricType | undefined;
|
||||
}
|
||||
|
||||
export interface AllAttributesValueProps {
|
||||
@@ -41,38 +36,3 @@ export interface AllAttributesValueProps {
|
||||
filterValue: string[];
|
||||
goToMetricsExploreWithAppliedAttribute: (key: string, value: string) => void;
|
||||
}
|
||||
|
||||
export interface AllAttributesEmptyTextProps {
|
||||
isErrorAttributes: boolean;
|
||||
refetchAttributes: () => void;
|
||||
}
|
||||
|
||||
export type MetricHighlight = MetricsexplorertypesMetricHighlightsResponseDTO;
|
||||
|
||||
export type MetricAlert = MetricsexplorertypesMetricAlertDTO;
|
||||
|
||||
export type MetricDashboard = MetricsexplorertypesMetricDashboardDTO;
|
||||
|
||||
export type MetricMetadata = MetricsexplorertypesMetricMetadataDTO;
|
||||
export interface MetricMetadataFormState {
|
||||
type: MetrictypesTypeDTO;
|
||||
description: string;
|
||||
temporality?: MetrictypesTemporalityDTO;
|
||||
unit: string;
|
||||
isMonotonic: boolean;
|
||||
}
|
||||
|
||||
export type MetricAttribute = MetricsexplorertypesMetricAttributeDTO;
|
||||
|
||||
export enum TableFields {
|
||||
DESCRIPTION = 'description',
|
||||
UNIT = 'unit',
|
||||
TYPE = 'type',
|
||||
Temporality = 'temporality',
|
||||
IS_MONOTONIC = 'isMonotonic',
|
||||
}
|
||||
|
||||
export interface MetricDetailsErrorStateProps {
|
||||
refetch?: () => void;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
import { UpdateMetricMetadataMutationBody } from 'api/generated/services/metrics';
|
||||
import {
|
||||
GetMetricMetadata200,
|
||||
MetrictypesTemporalityDTO,
|
||||
MetrictypesTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { Temporality } from 'api/metricsExplorer/getMetricDetails';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import { SpaceAggregation, TimeAggregation } from 'api/v5/v5';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
|
||||
import { MetricMetadata, MetricMetadataFormState } from './types';
|
||||
|
||||
export function formatTimestampToReadableDate(
|
||||
timestamp: number | string | undefined,
|
||||
): string {
|
||||
if (!timestamp) {
|
||||
return '-';
|
||||
}
|
||||
export function formatTimestampToReadableDate(timestamp: string): string {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
||||
@@ -50,10 +39,7 @@ export function formatTimestampToReadableDate(
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
export function formatNumberToCompactFormat(num: number | undefined): string {
|
||||
if (!num) {
|
||||
return '-';
|
||||
}
|
||||
export function formatNumberToCompactFormat(num: number): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
notation: 'compact',
|
||||
maximumFractionDigits: 1,
|
||||
@@ -61,30 +47,27 @@ export function formatNumberToCompactFormat(num: number | undefined): string {
|
||||
}
|
||||
|
||||
export function determineIsMonotonic(
|
||||
metricType: MetrictypesTypeDTO,
|
||||
temporality?: MetrictypesTemporalityDTO,
|
||||
metricType: MetricType,
|
||||
temporality?: Temporality,
|
||||
): boolean {
|
||||
if (
|
||||
metricType === MetrictypesTypeDTO.histogram ||
|
||||
metricType === MetrictypesTypeDTO.exponentialhistogram
|
||||
metricType === MetricType.HISTOGRAM ||
|
||||
metricType === MetricType.EXPONENTIAL_HISTOGRAM
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
metricType === MetrictypesTypeDTO.gauge ||
|
||||
metricType === MetrictypesTypeDTO.summary
|
||||
) {
|
||||
if (metricType === MetricType.GAUGE || metricType === MetricType.SUMMARY) {
|
||||
return false;
|
||||
}
|
||||
if (metricType === MetrictypesTypeDTO.sum) {
|
||||
return temporality === MetrictypesTemporalityDTO.cumulative;
|
||||
if (metricType === MetricType.SUM) {
|
||||
return temporality === Temporality.CUMULATIVE;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getMetricDetailsQuery(
|
||||
metricName: string,
|
||||
metricType: MetrictypesTypeDTO | undefined,
|
||||
metricType: MetricType | undefined,
|
||||
filter?: { key: string; value: string },
|
||||
groupBy?: string,
|
||||
): Query {
|
||||
@@ -92,23 +75,23 @@ export function getMetricDetailsQuery(
|
||||
let spaceAggregation;
|
||||
let aggregateOperator;
|
||||
switch (metricType) {
|
||||
case MetrictypesTypeDTO.sum:
|
||||
case MetricType.SUM:
|
||||
timeAggregation = 'rate';
|
||||
spaceAggregation = 'sum';
|
||||
aggregateOperator = 'rate';
|
||||
break;
|
||||
case MetrictypesTypeDTO.gauge:
|
||||
case MetricType.GAUGE:
|
||||
timeAggregation = 'avg';
|
||||
spaceAggregation = 'avg';
|
||||
aggregateOperator = 'avg';
|
||||
break;
|
||||
case MetrictypesTypeDTO.summary:
|
||||
case MetricType.SUMMARY:
|
||||
timeAggregation = 'noop';
|
||||
spaceAggregation = 'sum';
|
||||
aggregateOperator = 'noop';
|
||||
break;
|
||||
case MetrictypesTypeDTO.histogram:
|
||||
case MetrictypesTypeDTO.exponentialhistogram:
|
||||
case MetricType.HISTOGRAM:
|
||||
case MetricType.EXPONENTIAL_HISTOGRAM:
|
||||
timeAggregation = 'noop';
|
||||
spaceAggregation = 'p90';
|
||||
aggregateOperator = 'noop';
|
||||
@@ -177,38 +160,3 @@ export function getMetricDetailsQuery(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function transformMetricMetadata(
|
||||
apiData: GetMetricMetadata200 | undefined,
|
||||
): MetricMetadata | null {
|
||||
if (!apiData || !apiData.data) {
|
||||
return null;
|
||||
}
|
||||
const { type, description, unit, temporality, isMonotonic } = apiData.data;
|
||||
|
||||
return {
|
||||
type,
|
||||
description,
|
||||
unit,
|
||||
temporality,
|
||||
isMonotonic,
|
||||
};
|
||||
}
|
||||
|
||||
export function transformUpdateMetricMetadataRequest(
|
||||
metricName: string,
|
||||
metricMetadata: MetricMetadataFormState,
|
||||
): UpdateMetricMetadataMutationBody {
|
||||
return {
|
||||
metricName: metricName,
|
||||
type: metricMetadata.type,
|
||||
description: metricMetadata.description,
|
||||
unit: metricMetadata.unit,
|
||||
temporality:
|
||||
metricMetadata.temporality ?? MetrictypesTemporalityDTO.unspecified,
|
||||
isMonotonic: determineIsMonotonic(
|
||||
metricMetadata.type,
|
||||
metricMetadata.temporality,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
|
||||
import NoLogs from 'container/NoLogs/NoLogs';
|
||||
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
|
||||
@@ -129,7 +128,7 @@ function Summary(): JSX.Element {
|
||||
} = useGetMetricsList(metricsListQuery, {
|
||||
enabled: !!metricsListQuery && !isInspectModalOpen,
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_METRICS_LIST,
|
||||
'metricsList',
|
||||
queryFiltersWithoutId,
|
||||
orderBy,
|
||||
pageSize,
|
||||
@@ -324,7 +323,7 @@ function Summary(): JSX.Element {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{isMetricDetailsOpen && selectedMetricName && (
|
||||
{isMetricDetailsOpen && (
|
||||
<MetricDetails
|
||||
isOpen={isMetricDetailsOpen}
|
||||
onClose={closeMetricDetails}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
|
||||
import { TreemapViewType } from './types';
|
||||
@@ -26,16 +25,7 @@ export const METRIC_TYPE_LABEL_MAP = {
|
||||
[MetricType.EXPONENTIAL_HISTOGRAM]: 'Exp. Histogram',
|
||||
};
|
||||
|
||||
export const METRIC_TYPE_VIEW_LABEL_MAP: Record<MetrictypesTypeDTO, string> = {
|
||||
[MetrictypesTypeDTO.sum]: 'Sum',
|
||||
[MetrictypesTypeDTO.gauge]: 'Gauge',
|
||||
[MetrictypesTypeDTO.histogram]: 'Histogram',
|
||||
[MetrictypesTypeDTO.summary]: 'Summary',
|
||||
[MetrictypesTypeDTO.exponentialhistogram]: 'Exp. Histogram',
|
||||
};
|
||||
|
||||
// TODO(@amlannandy): To remove this once API migration is complete
|
||||
export const METRIC_TYPE_VALUES_MAP: Record<MetricType, string> = {
|
||||
export const METRIC_TYPE_VALUES_MAP = {
|
||||
[MetricType.SUM]: 'Sum',
|
||||
[MetricType.GAUGE]: 'Gauge',
|
||||
[MetricType.HISTOGRAM]: 'Histogram',
|
||||
@@ -43,14 +33,6 @@ export const METRIC_TYPE_VALUES_MAP: Record<MetricType, string> = {
|
||||
[MetricType.EXPONENTIAL_HISTOGRAM]: 'ExponentialHistogram',
|
||||
};
|
||||
|
||||
export const METRIC_TYPE_VIEW_VALUES_MAP: Record<MetrictypesTypeDTO, string> = {
|
||||
[MetrictypesTypeDTO.sum]: 'Sum',
|
||||
[MetrictypesTypeDTO.gauge]: 'Gauge',
|
||||
[MetrictypesTypeDTO.histogram]: 'Histogram',
|
||||
[MetrictypesTypeDTO.summary]: 'Summary',
|
||||
[MetrictypesTypeDTO.exponentialhistogram]: 'ExponentialHistogram',
|
||||
};
|
||||
|
||||
export const IS_METRIC_DETAILS_OPEN_KEY = 'isMetricDetailsOpen';
|
||||
export const IS_INSPECT_MODAL_OPEN_KEY = 'isInspectModalOpen';
|
||||
export const SELECTED_METRIC_NAME_KEY = 'selectedMetricName';
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useMemo } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Tooltip, Typography } from 'antd';
|
||||
import { ColumnType } from 'antd/es/table';
|
||||
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
MetricsListItemData,
|
||||
MetricsListPayload,
|
||||
@@ -22,7 +21,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { METRIC_TYPE_LABEL_MAP, METRIC_TYPE_VIEW_LABEL_MAP } from './constants';
|
||||
import { METRIC_TYPE_LABEL_MAP } from './constants';
|
||||
import MetricNameSearch from './MetricNameSearch';
|
||||
import MetricTypeSearch from './MetricTypeSearch';
|
||||
import { MetricsListItemRowData, TreemapTile, TreemapViewType } from './types';
|
||||
@@ -144,66 +143,6 @@ export function MetricTypeRenderer({
|
||||
);
|
||||
}
|
||||
|
||||
export function MetricTypeViewRenderer({
|
||||
type,
|
||||
}: {
|
||||
type: MetrictypesTypeDTO;
|
||||
}): JSX.Element {
|
||||
const [icon, color] = useMemo(() => {
|
||||
switch (type) {
|
||||
case MetrictypesTypeDTO.sum:
|
||||
return [
|
||||
<Diff key={type} size={12} color={Color.BG_ROBIN_500} />,
|
||||
Color.BG_ROBIN_500,
|
||||
];
|
||||
case MetrictypesTypeDTO.gauge:
|
||||
return [
|
||||
<Gauge key={type} size={12} color={Color.BG_SAKURA_500} />,
|
||||
Color.BG_SAKURA_500,
|
||||
];
|
||||
case MetrictypesTypeDTO.histogram:
|
||||
return [
|
||||
<BarChart2 key={type} size={12} color={Color.BG_SIENNA_500} />,
|
||||
Color.BG_SIENNA_500,
|
||||
];
|
||||
case MetrictypesTypeDTO.summary:
|
||||
return [
|
||||
<BarChartHorizontal key={type} size={12} color={Color.BG_FOREST_500} />,
|
||||
Color.BG_FOREST_500,
|
||||
];
|
||||
case MetrictypesTypeDTO.exponentialhistogram:
|
||||
return [
|
||||
<BarChart key={type} size={12} color={Color.BG_AQUA_500} />,
|
||||
Color.BG_AQUA_500,
|
||||
];
|
||||
default:
|
||||
return [null, ''];
|
||||
}
|
||||
}, [type]);
|
||||
|
||||
const metricTypeRendererStyle = useMemo(
|
||||
() => ({
|
||||
backgroundColor: `${color}33`,
|
||||
border: `1px solid ${color}`,
|
||||
color,
|
||||
}),
|
||||
[color],
|
||||
);
|
||||
|
||||
const metricTypeRendererTextStyle = useMemo(() => ({ color, fontSize: 12 }), [
|
||||
color,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="metric-type-renderer" style={metricTypeRendererStyle}>
|
||||
{icon}
|
||||
<Typography.Text style={metricTypeRendererTextStyle}>
|
||||
{METRIC_TYPE_VIEW_LABEL_MAP[type]}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ValidateRowValueWrapper({
|
||||
value,
|
||||
children,
|
||||
@@ -221,9 +160,6 @@ export const formatNumberIntoHumanReadableFormat = (
|
||||
num: number,
|
||||
addPlusSign = true,
|
||||
): string => {
|
||||
if (!num) {
|
||||
return '-';
|
||||
}
|
||||
function format(num: number, divisor: number, suffix: string): string {
|
||||
const value = num / divisor;
|
||||
return value % 1 === 0
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { ColorPickerProps } from 'antd';
|
||||
import { Color } from 'antd/es/color-picker';
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import LegendColors from './LegendColors';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
__esModule: true,
|
||||
useQueryBuilder: (): { currentQuery: unknown } => ({
|
||||
currentQuery: {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
queryName: 'A',
|
||||
legend: '{service.name}',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('antd', () => {
|
||||
const actual = jest.requireActual('antd');
|
||||
return {
|
||||
...actual,
|
||||
ColorPicker: ({ onChange, children }: ColorPickerProps): JSX.Element => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="legend-color-picker"
|
||||
onClick={(): void =>
|
||||
onChange!({ toHexString: (): string => '#ffffff' } as Color, '#ffffff')
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
describe('LegendColors', () => {
|
||||
it('renders legend colors panel and items', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(
|
||||
<LegendColors
|
||||
customLegendColors={{}}
|
||||
setCustomLegendColors={jest.fn()}
|
||||
queryResponse={undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Legend Colors')).toBeInTheDocument();
|
||||
|
||||
// Expand the collapse to reveal legend items
|
||||
await user.click(
|
||||
screen.getByRole('tab', {
|
||||
name: /Legend Colors/i,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(screen.getByText('{service.name}')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls setCustomLegendColors when color is changed', async () => {
|
||||
const user = userEvent.setup();
|
||||
const setCustomLegendColors = jest.fn();
|
||||
|
||||
render(
|
||||
<LegendColors
|
||||
customLegendColors={{}}
|
||||
setCustomLegendColors={setCustomLegendColors}
|
||||
queryResponse={undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Expand to render the mocked ColorPicker button
|
||||
await user.click(
|
||||
screen.getByRole('tab', {
|
||||
name: /Legend Colors/i,
|
||||
}),
|
||||
);
|
||||
|
||||
const colorTrigger = screen.getByTestId('legend-color-picker');
|
||||
|
||||
await user.click(colorTrigger);
|
||||
|
||||
expect(setCustomLegendColors).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('throttles rapid color changes', async () => {
|
||||
jest.useFakeTimers();
|
||||
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
|
||||
const setCustomLegendColors = jest.fn();
|
||||
|
||||
render(
|
||||
<LegendColors
|
||||
customLegendColors={{}}
|
||||
setCustomLegendColors={setCustomLegendColors}
|
||||
queryResponse={undefined}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Expand panel to render the mocked ColorPicker button
|
||||
await user.click(
|
||||
screen.getByRole('tab', {
|
||||
name: /Legend Colors/i,
|
||||
}),
|
||||
);
|
||||
|
||||
const colorTrigger = screen.getByTestId('legend-color-picker');
|
||||
|
||||
// Fire multiple rapid changes
|
||||
await user.click(colorTrigger);
|
||||
await user.click(colorTrigger);
|
||||
await user.click(colorTrigger);
|
||||
await user.click(colorTrigger);
|
||||
|
||||
// Flush pending throttled calls
|
||||
jest.advanceTimersByTime(500);
|
||||
|
||||
// Throttling should ensure we don't invoke the setter once per click
|
||||
expect(setCustomLegendColors).toHaveBeenCalledTimes(2);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -14,7 +14,6 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { generateColor } from 'lib/uPlotLib/utils/generateColor';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import { Palette } from 'lucide-react';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
@@ -96,24 +95,13 @@ function LegendColors({
|
||||
);
|
||||
};
|
||||
|
||||
// Handle color change (throttled to avoid excessive updates)
|
||||
const handleColorChange = useMemo(
|
||||
() =>
|
||||
throttle((label: string, color: string): void => {
|
||||
setCustomLegendColors((prev) => ({
|
||||
...prev,
|
||||
[label]: color,
|
||||
}));
|
||||
}, 200), // 200ms is a good compromise between responsiveness and performance
|
||||
[setCustomLegendColors],
|
||||
);
|
||||
|
||||
// Clean up throttled handler on unmount
|
||||
useEffect(() => {
|
||||
return (): void => {
|
||||
handleColorChange.cancel();
|
||||
};
|
||||
}, [handleColorChange]);
|
||||
// Handle color change
|
||||
const handleColorChange = (label: string, color: string): void => {
|
||||
setCustomLegendColors((prev) => ({
|
||||
...prev,
|
||||
[label]: color,
|
||||
}));
|
||||
};
|
||||
|
||||
// Reset to default color
|
||||
const resetToDefault = (label: string): void => {
|
||||
|
||||
@@ -169,10 +169,6 @@
|
||||
font-weight: 400;
|
||||
line-height: 16px; /* 133.333% */
|
||||
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-select-selector {
|
||||
border: none;
|
||||
height: unset;
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
import { useMemo, useRef, useState } from 'react';
|
||||
import { useDrag, useDrop, XYCoord } from 'react-dnd';
|
||||
import { Button, Input, InputNumber, Select, Space, Typography } from 'antd';
|
||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { unitOptions } from 'container/NewWidget/utils';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -207,18 +204,6 @@ function Threshold({
|
||||
return unit !== 'none' && convertUnit(value, unit, toUnitId) === null;
|
||||
}, [selectedGraph, yAxisUnit, tableSelectedOption, columnUnits, unit, value]);
|
||||
|
||||
const unitSelectCategories = useMemo(() => {
|
||||
return unitOptions(
|
||||
selectedGraph === PANEL_TYPES.TABLE
|
||||
? getColumnUnit(tableSelectedOption, columnUnits || {}) || ''
|
||||
: yAxisUnit || '',
|
||||
);
|
||||
}, [selectedGraph, yAxisUnit, tableSelectedOption, columnUnits]);
|
||||
|
||||
const unitLabel = useMemo(() => {
|
||||
return Y_AXIS_UNIT_NAMES[unit as keyof typeof Y_AXIS_UNIT_NAMES];
|
||||
}, [unit]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={allowDragAndDrop ? ref : null}
|
||||
@@ -328,17 +313,19 @@ function Threshold({
|
||||
<ShowCaseValue value={value} className="unit-input" />
|
||||
)}
|
||||
{isEditMode ? (
|
||||
<YAxisUnitSelector
|
||||
value={unit}
|
||||
<Select
|
||||
defaultValue={unit}
|
||||
options={unitOptions(
|
||||
selectedGraph === PANEL_TYPES.TABLE
|
||||
? getColumnUnit(tableSelectedOption, columnUnits || {}) || ''
|
||||
: yAxisUnit || '',
|
||||
)}
|
||||
onChange={handleUnitChange}
|
||||
placeholder="Select unit"
|
||||
source={YAxisSource.DASHBOARDS}
|
||||
initialValue={unit}
|
||||
categoriesOverride={unitSelectCategories}
|
||||
containerClassName="unit-selection"
|
||||
showSearch
|
||||
className="unit-selection"
|
||||
/>
|
||||
) : (
|
||||
<ShowCaseValue value={unitLabel} className="unit-selection-prev" />
|
||||
<ShowCaseValue value={unit} className="unit-selection-prev" />
|
||||
)}
|
||||
</div>
|
||||
<div className="thresholds-color-selector">
|
||||
@@ -369,10 +356,7 @@ function Threshold({
|
||||
)}
|
||||
</div>
|
||||
{isInvalidUnitComparison && (
|
||||
<Typography.Text
|
||||
className="invalid-unit"
|
||||
data-testid="invalid-unit-comparison"
|
||||
>
|
||||
<Typography.Text className="invalid-unit">
|
||||
Threshold unit ({unit}) is not valid in comparison with the{' '}
|
||||
{selectedGraph === PANEL_TYPES.TABLE ? 'column' : 'y-axis'} unit (
|
||||
{selectedGraph === PANEL_TYPES.TABLE
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
@@ -16,26 +14,12 @@ jest.mock('lib/query/createTableColumnsFromQuery', () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the unitOptions function to return YAxisCategory-shaped data
|
||||
// Mock the unitOptions function
|
||||
jest.mock('container/NewWidget/utils', () => ({
|
||||
unitOptions: jest.fn(() => [
|
||||
{
|
||||
name: 'Mock Category',
|
||||
units: [
|
||||
{
|
||||
id: UniversalYAxisUnit.NONE,
|
||||
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.NONE],
|
||||
},
|
||||
{
|
||||
id: UniversalYAxisUnit.PERCENT,
|
||||
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.PERCENT],
|
||||
},
|
||||
{
|
||||
id: UniversalYAxisUnit.MILLISECONDS,
|
||||
name: Y_AXIS_UNIT_NAMES[UniversalYAxisUnit.MILLISECONDS],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ value: 'none', label: 'None' },
|
||||
{ value: '%', label: 'Percent (0 - 100)' },
|
||||
{ value: 'ms', label: 'Milliseconds (ms)' },
|
||||
]),
|
||||
}));
|
||||
|
||||
@@ -44,7 +28,7 @@ const defaultProps = {
|
||||
keyIndex: 0,
|
||||
thresholdOperator: '>' as const,
|
||||
thresholdValue: 50,
|
||||
thresholdUnit: UniversalYAxisUnit.NONE,
|
||||
thresholdUnit: 'none',
|
||||
thresholdColor: 'Red',
|
||||
thresholdFormat: 'Text' as const,
|
||||
isEditEnabled: true,
|
||||
@@ -54,11 +38,8 @@ const defaultProps = {
|
||||
{ value: 'memory_usage', label: 'Memory Usage' },
|
||||
],
|
||||
thresholdTableOptions: 'cpu_usage',
|
||||
columnUnits: {
|
||||
cpu_usage: UniversalYAxisUnit.PERCENT,
|
||||
memory_usage: UniversalYAxisUnit.BYTES,
|
||||
},
|
||||
yAxisUnit: UniversalYAxisUnit.PERCENT,
|
||||
columnUnits: { cpu_usage: 'percent', memory_usage: 'bytes' },
|
||||
yAxisUnit: '%',
|
||||
moveThreshold: jest.fn(),
|
||||
};
|
||||
|
||||
@@ -87,27 +68,28 @@ describe('Threshold Component Unit Validation', () => {
|
||||
it('should show validation error when threshold unit is not "none" and units are incompatible', () => {
|
||||
// Act - Render component with incompatible units (ms vs percent)
|
||||
renderThreshold({
|
||||
thresholdUnit: UniversalYAxisUnit.MILLISECONDS,
|
||||
thresholdUnit: 'ms',
|
||||
thresholdValue: 50,
|
||||
});
|
||||
|
||||
const errorMessage = screen.getByTestId('invalid-unit-comparison');
|
||||
// Assert - Validation error should be displayed
|
||||
expect(errorMessage.textContent).toBe(
|
||||
`Threshold unit (${UniversalYAxisUnit.MILLISECONDS}) is not valid in comparison with the column unit (${UniversalYAxisUnit.PERCENT})`,
|
||||
);
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Threshold unit \(ms\) is not valid in comparison with the column unit \(percent\)/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show validation error when threshold unit matches column unit', () => {
|
||||
// Act - Render component with matching units
|
||||
renderThreshold({
|
||||
thresholdUnit: UniversalYAxisUnit.PERCENT,
|
||||
thresholdUnit: 'percent',
|
||||
thresholdValue: 50,
|
||||
});
|
||||
|
||||
// Assert - No validation error should be displayed
|
||||
expect(
|
||||
screen.queryByTestId('invalid-unit-comparison'),
|
||||
screen.queryByText(/Threshold unit.*is not valid in comparison/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -115,16 +97,17 @@ describe('Threshold Component Unit Validation', () => {
|
||||
// Act - Render component for time series with incompatible units
|
||||
renderThreshold({
|
||||
selectedGraph: PANEL_TYPES.TIME_SERIES,
|
||||
thresholdUnit: UniversalYAxisUnit.MILLISECONDS,
|
||||
thresholdUnit: 'ms',
|
||||
thresholdValue: 100,
|
||||
yAxisUnit: UniversalYAxisUnit.PERCENT,
|
||||
yAxisUnit: 'percent',
|
||||
});
|
||||
|
||||
const errorMessage = screen.getByTestId('invalid-unit-comparison');
|
||||
// Assert - Validation error should be displayed
|
||||
expect(errorMessage.textContent).toBe(
|
||||
`Threshold unit (${UniversalYAxisUnit.MILLISECONDS}) is not valid in comparison with the y-axis unit (${UniversalYAxisUnit.PERCENT})`,
|
||||
);
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Threshold unit \(ms\) is not valid in comparison with the y-axis unit \(percent\)/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show validation error for time series graph when threshold unit is "none"', () => {
|
||||
@@ -133,39 +116,43 @@ describe('Threshold Component Unit Validation', () => {
|
||||
selectedGraph: PANEL_TYPES.TIME_SERIES,
|
||||
thresholdUnit: 'none',
|
||||
thresholdValue: 100,
|
||||
yAxisUnit: UniversalYAxisUnit.PERCENT,
|
||||
yAxisUnit: 'percent',
|
||||
});
|
||||
|
||||
// Assert - No validation error should be displayed
|
||||
expect(
|
||||
screen.queryByTestId('invalid-unit-comparison'),
|
||||
screen.queryByText(/Threshold unit.*is not valid in comparison/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not show validation error when threshold unit is compatible with column unit', () => {
|
||||
// Act - Render component with compatible units (both in same category - Time)
|
||||
renderThreshold({
|
||||
thresholdUnit: UniversalYAxisUnit.SECONDS,
|
||||
thresholdUnit: 's',
|
||||
thresholdValue: 100,
|
||||
columnUnits: { cpu_usage: UniversalYAxisUnit.MILLISECONDS },
|
||||
columnUnits: { cpu_usage: 'ms' },
|
||||
thresholdTableOptions: 'cpu_usage',
|
||||
});
|
||||
|
||||
// Assert - No validation error should be displayed
|
||||
expect(
|
||||
screen.queryByTestId('invalid-unit-comparison'),
|
||||
screen.queryByText(/Threshold unit.*is not valid in comparison/i),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show validation error when threshold unit is in different category than column unit', () => {
|
||||
// Act - Render component with units from different categories
|
||||
renderThreshold({
|
||||
thresholdUnit: UniversalYAxisUnit.BYTES,
|
||||
thresholdUnit: 'bytes',
|
||||
thresholdValue: 100,
|
||||
yAxisUnit: UniversalYAxisUnit.PERCENT,
|
||||
yAxisUnit: 'percent',
|
||||
});
|
||||
|
||||
const errorMessage = screen.getByTestId('invalid-unit-comparison');
|
||||
// Assert - Validation error should be displayed
|
||||
expect(errorMessage.textContent).toBe(
|
||||
`Threshold unit (${UniversalYAxisUnit.BYTES}) is not valid in comparison with the column unit (${UniversalYAxisUnit.PERCENT})`,
|
||||
);
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Threshold unit \(bytes\) is not valid in comparison with the column unit \(percent\)/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { Layout } from 'react-grid-layout';
|
||||
import { DefaultOptionType } from 'antd/es/select';
|
||||
import { omitIdFromQuery } from 'components/ExplorerCard/utils';
|
||||
import { PrecisionOptionsEnum } from 'components/Graph/types';
|
||||
import { YAxisCategoryNames } from 'components/YAxisUnitSelector/constants';
|
||||
import {
|
||||
UniversalYAxisUnit,
|
||||
YAxisCategory,
|
||||
YAxisSource,
|
||||
} from 'components/YAxisUnitSelector/types';
|
||||
import { YAxisSource } from 'components/YAxisUnitSelector/types';
|
||||
import { getYAxisCategories } from 'components/YAxisUnitSelector/utils';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
@@ -609,7 +606,7 @@ export const PANEL_TYPE_TO_QUERY_TYPES: Record<PANEL_TYPES, EQueryType[]> = {
|
||||
*/
|
||||
export const getCategorySelectOptionByName = (
|
||||
name?: YAxisCategoryNames,
|
||||
): { name: string; id: UniversalYAxisUnit }[] => {
|
||||
): DefaultOptionType[] => {
|
||||
const categories = getYAxisCategories(YAxisSource.DASHBOARDS);
|
||||
if (!categories.length) {
|
||||
return [];
|
||||
@@ -618,8 +615,8 @@ export const getCategorySelectOptionByName = (
|
||||
categories
|
||||
.find((category) => category.name === name)
|
||||
?.units.map((unit) => ({
|
||||
name: unit.name,
|
||||
id: unit.id,
|
||||
label: unit.name,
|
||||
value: unit.id,
|
||||
})) || []
|
||||
);
|
||||
};
|
||||
@@ -631,19 +628,19 @@ export const getCategorySelectOptionByName = (
|
||||
* select options. If a valid category is found, it filters the supported categories
|
||||
* to return only the options for the matched category.
|
||||
*/
|
||||
export const unitOptions = (columnUnit: string): YAxisCategory[] => {
|
||||
export const unitOptions = (columnUnit: string): DefaultOptionType[] => {
|
||||
const category = getCategoryName(columnUnit);
|
||||
if (isEmpty(category)) {
|
||||
return categoryToSupport.map((category) => ({
|
||||
name: category,
|
||||
units: getCategorySelectOptionByName(category),
|
||||
label: category,
|
||||
options: getCategorySelectOptionByName(category),
|
||||
}));
|
||||
}
|
||||
return categoryToSupport
|
||||
.filter((supportedCategory) => supportedCategory === category)
|
||||
.map((filteredCategory) => ({
|
||||
name: filteredCategory,
|
||||
units: getCategorySelectOptionByName(filteredCategory),
|
||||
label: filteredCategory,
|
||||
options: getCategorySelectOptionByName(filteredCategory),
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form } from 'antd';
|
||||
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -30,7 +30,7 @@ function TagFilterInput({
|
||||
};
|
||||
|
||||
return (
|
||||
<QueryBuilderSearchV2
|
||||
<QueryBuilderSearch
|
||||
query={query}
|
||||
onChange={onQueryChange}
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -86,7 +86,7 @@ jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
|
||||
}));
|
||||
|
||||
const BASE_URL = ENVIRONMENT.baseURL;
|
||||
const attributeKeysURL = `${BASE_URL}/api/v3/filter_suggestions`;
|
||||
const attributeKeysURL = `${BASE_URL}/api/v3/autocomplete/attribute_keys`;
|
||||
|
||||
describe('PipelinePage container test', () => {
|
||||
beforeAll(() => {
|
||||
@@ -333,34 +333,26 @@ describe('PipelinePage container test', () => {
|
||||
ctx.json({
|
||||
status: 'success',
|
||||
data: {
|
||||
attributes: [
|
||||
attributeKeys: [
|
||||
{
|
||||
key: 'otelServiceName',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
{
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
{
|
||||
key: 'service.instance.id',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
{
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
{
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'tag',
|
||||
isColumn: false,
|
||||
isJSON: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -973,7 +973,6 @@ function QueryBuilderSearchV2(
|
||||
return (
|
||||
<div className="query-builder-search-v2">
|
||||
<Select
|
||||
data-testid={'qb-search-select'}
|
||||
ref={selectRef}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...(hasPopupContainer ? { getPopupContainer: popupContainer } : {})}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
|
||||
import useDashboardsListQueryParams, {
|
||||
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
|
||||
IDashboardsListQueryParams,
|
||||
} from '../useDashboardsListQueryParams';
|
||||
|
||||
const mockSafeNavigate = jest.fn();
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: mockSafeNavigate,
|
||||
}),
|
||||
}));
|
||||
|
||||
function makeWrapper(
|
||||
initialUrl: string,
|
||||
): ({ children }: { children: React.ReactNode }) => JSX.Element {
|
||||
return function Wrapper({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
return React.createElement(
|
||||
MemoryRouter,
|
||||
{ initialEntries: [initialUrl] },
|
||||
children,
|
||||
) as JSX.Element;
|
||||
};
|
||||
}
|
||||
|
||||
describe('useDashboardsListQueryParams', () => {
|
||||
beforeEach(() => {
|
||||
mockSafeNavigate.mockClear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
describe('initialisation from URL params', () => {
|
||||
it('returns default params when no URL query params are present', () => {
|
||||
const { result } = renderHook(() => useDashboardsListQueryParams(), {
|
||||
wrapper: makeWrapper('/dashboard'),
|
||||
});
|
||||
|
||||
expect(result.current.dashboardsListQueryParams).toEqual({
|
||||
columnKey: 'updatedAt',
|
||||
order: 'descend',
|
||||
page: '1',
|
||||
search: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('reads valid columnKey, order, page and search from the URL', () => {
|
||||
const { result } = renderHook(() => useDashboardsListQueryParams(), {
|
||||
wrapper: makeWrapper(
|
||||
'/dashboard?columnKey=createdAt&order=ascend&page=3&search=foo',
|
||||
),
|
||||
});
|
||||
|
||||
expect(result.current.dashboardsListQueryParams).toEqual({
|
||||
columnKey: 'createdAt',
|
||||
order: 'ascend',
|
||||
page: '3',
|
||||
search: 'foo',
|
||||
});
|
||||
});
|
||||
|
||||
it('falls back to updatedAt for an unsupported columnKey', () => {
|
||||
const { result } = renderHook(() => useDashboardsListQueryParams(), {
|
||||
wrapper: makeWrapper('/dashboard?columnKey=name&order=ascend'),
|
||||
});
|
||||
|
||||
expect(result.current.dashboardsListQueryParams.columnKey).toBe('updatedAt');
|
||||
});
|
||||
|
||||
it('falls back to descend for an unsupported order', () => {
|
||||
const { result } = renderHook(() => useDashboardsListQueryParams(), {
|
||||
wrapper: makeWrapper('/dashboard?columnKey=createdAt&order=invalid'),
|
||||
});
|
||||
|
||||
expect(result.current.dashboardsListQueryParams.order).toBe('descend');
|
||||
});
|
||||
|
||||
it('defaults page to 1 when page param is absent', () => {
|
||||
const { result } = renderHook(() => useDashboardsListQueryParams(), {
|
||||
wrapper: makeWrapper('/dashboard'),
|
||||
});
|
||||
|
||||
expect(result.current.dashboardsListQueryParams.page).toBe('1');
|
||||
});
|
||||
|
||||
it('defaults search to empty string when search param is absent', () => {
|
||||
const { result } = renderHook(() => useDashboardsListQueryParams(), {
|
||||
wrapper: makeWrapper('/dashboard'),
|
||||
});
|
||||
|
||||
expect(result.current.dashboardsListQueryParams.search).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateDashboardsListQueryParams', () => {
|
||||
it('updates the state when params change', () => {
|
||||
const { result } = renderHook(() => useDashboardsListQueryParams(), {
|
||||
wrapper: makeWrapper('/dashboard'),
|
||||
});
|
||||
|
||||
const updated: IDashboardsListQueryParams = {
|
||||
columnKey: 'createdAt',
|
||||
order: 'ascend',
|
||||
page: '2',
|
||||
search: 'signoz',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateDashboardsListQueryParams(updated);
|
||||
});
|
||||
|
||||
expect(result.current.dashboardsListQueryParams).toEqual(updated);
|
||||
});
|
||||
|
||||
it('does not update state when params are identical', () => {
|
||||
const { result } = renderHook(() => useDashboardsListQueryParams(), {
|
||||
wrapper: makeWrapper('/dashboard'),
|
||||
});
|
||||
|
||||
const initial = result.current.dashboardsListQueryParams;
|
||||
|
||||
act(() => {
|
||||
result.current.updateDashboardsListQueryParams({ ...initial });
|
||||
});
|
||||
|
||||
// Reference equality confirms no re-render-triggering state update.
|
||||
expect(result.current.dashboardsListQueryParams).toBe(initial);
|
||||
});
|
||||
|
||||
it('calls safeNavigate with the updated search string', () => {
|
||||
const { result } = renderHook(() => useDashboardsListQueryParams(), {
|
||||
wrapper: makeWrapper('/dashboard'),
|
||||
});
|
||||
|
||||
const updated: IDashboardsListQueryParams = {
|
||||
columnKey: 'createdAt',
|
||||
order: 'ascend',
|
||||
page: '2',
|
||||
search: 'test',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateDashboardsListQueryParams(updated);
|
||||
});
|
||||
|
||||
expect(mockSafeNavigate).toHaveBeenCalledTimes(1);
|
||||
const [navigateArg] = mockSafeNavigate.mock.calls[0];
|
||||
const searchParams = new URLSearchParams(navigateArg.search);
|
||||
expect(searchParams.get('columnKey')).toBe('createdAt');
|
||||
expect(searchParams.get('order')).toBe('ascend');
|
||||
expect(searchParams.get('page')).toBe('2');
|
||||
expect(searchParams.get('search')).toBe('test');
|
||||
});
|
||||
|
||||
it('persists params to sessionStorage', () => {
|
||||
const { result } = renderHook(() => useDashboardsListQueryParams(), {
|
||||
wrapper: makeWrapper('/dashboard'),
|
||||
});
|
||||
|
||||
const updated: IDashboardsListQueryParams = {
|
||||
columnKey: 'updatedAt',
|
||||
order: 'descend',
|
||||
page: '1',
|
||||
search: 'signoz',
|
||||
};
|
||||
|
||||
act(() => {
|
||||
result.current.updateDashboardsListQueryParams(updated);
|
||||
});
|
||||
|
||||
const stored = sessionStorage.getItem(
|
||||
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
|
||||
);
|
||||
expect(JSON.parse(stored || '')).toEqual(updated);
|
||||
});
|
||||
|
||||
it('still calls safeNavigate even when params are unchanged', () => {
|
||||
const { result } = renderHook(() => useDashboardsListQueryParams(), {
|
||||
wrapper: makeWrapper('/dashboard'),
|
||||
});
|
||||
|
||||
const initial = result.current.dashboardsListQueryParams;
|
||||
|
||||
act(() => {
|
||||
result.current.updateDashboardsListQueryParams({ ...initial });
|
||||
});
|
||||
|
||||
expect(mockSafeNavigate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
72
frontend/src/hooks/dashboard/useDashboardsListQueryParams.ts
Normal file
72
frontend/src/hooks/dashboard/useDashboardsListQueryParams.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useState } from 'react';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
|
||||
export interface IDashboardsListQueryParams {
|
||||
columnKey: string;
|
||||
order: string;
|
||||
page: string;
|
||||
search: string;
|
||||
}
|
||||
|
||||
export const DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY =
|
||||
'dashboardsListQueryParams';
|
||||
|
||||
const SUPPORTED_COLUMN_KEYS = ['createdAt', 'updatedAt'];
|
||||
const SUPPORTED_ORDER_KEYS = ['ascend', 'descend'];
|
||||
|
||||
function useDashboardsListQueryParams(): {
|
||||
dashboardsListQueryParams: IDashboardsListQueryParams;
|
||||
updateDashboardsListQueryParams: (
|
||||
dashboardsListQueryParams: IDashboardsListQueryParams,
|
||||
) => void;
|
||||
} {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const params = useUrlQuery();
|
||||
|
||||
const orderColumnKeyQueryParam = params.get('columnKey');
|
||||
const orderQueryParam = params.get('order');
|
||||
const pageQueryParam = params.get('page');
|
||||
const searchQueryParam = params.get('search');
|
||||
|
||||
const [
|
||||
dashboardsListQueryParams,
|
||||
setDashboardsListQueryParams,
|
||||
] = useState<IDashboardsListQueryParams>({
|
||||
columnKey:
|
||||
orderColumnKeyQueryParam &&
|
||||
SUPPORTED_COLUMN_KEYS.includes(orderColumnKeyQueryParam)
|
||||
? orderColumnKeyQueryParam
|
||||
: 'updatedAt',
|
||||
order:
|
||||
orderQueryParam && SUPPORTED_ORDER_KEYS.includes(orderQueryParam)
|
||||
? orderQueryParam
|
||||
: 'descend',
|
||||
page: pageQueryParam || '1',
|
||||
search: searchQueryParam || '',
|
||||
});
|
||||
|
||||
function updateDashboardsListQueryParams(
|
||||
updatedQueryParams: IDashboardsListQueryParams,
|
||||
): void {
|
||||
if (!isEqual(updatedQueryParams, dashboardsListQueryParams)) {
|
||||
setDashboardsListQueryParams(updatedQueryParams);
|
||||
}
|
||||
|
||||
params.set('columnKey', updatedQueryParams.columnKey);
|
||||
params.set('order', updatedQueryParams.order);
|
||||
params.set('page', updatedQueryParams.page || '1');
|
||||
params.set('search', updatedQueryParams.search || '');
|
||||
|
||||
safeNavigate({ search: params.toString() });
|
||||
sessionStorage.setItem(
|
||||
DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY,
|
||||
JSON.stringify(updatedQueryParams),
|
||||
);
|
||||
}
|
||||
|
||||
return { dashboardsListQueryParams, updateDashboardsListQueryParams };
|
||||
}
|
||||
|
||||
export default useDashboardsListQueryParams;
|
||||
@@ -1,17 +1,15 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { OPERATORS, QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { MetricsType } from 'container/MetricsApplication/constant';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
|
||||
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||
import { AppState } from 'store/reducers';
|
||||
@@ -56,20 +54,6 @@ export const useActiveLog = (): UseActiveLog => {
|
||||
|
||||
const [activeLog, setActiveLog] = useState<ILog | null>(null);
|
||||
|
||||
// Close drawer/clear active log when query in URL changes
|
||||
const urlQuery = useUrlQuery();
|
||||
const compositeQuery = urlQuery.get(QueryParams.compositeQuery) ?? '';
|
||||
const prevQueryRef = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (
|
||||
prevQueryRef.current !== null &&
|
||||
prevQueryRef.current !== compositeQuery
|
||||
) {
|
||||
setActiveLog(null);
|
||||
}
|
||||
prevQueryRef.current = compositeQuery;
|
||||
}, [compositeQuery]);
|
||||
|
||||
const onSetDetailedLogData = useCallback(
|
||||
(logData: ILog) => {
|
||||
dispatch({
|
||||
|
||||
@@ -1,37 +1,32 @@
|
||||
import { useQueries, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import {
|
||||
getGetMetricMetadataQueryKey,
|
||||
getMetricMetadata,
|
||||
} from 'api/generated/services/metrics';
|
||||
import { GetMetricMetadata200 } from 'api/generated/services/sigNoz.schemas';
|
||||
import { getMetricMetadata } from 'api/metricsExplorer/v2/getMetricMetadata';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { MetricMetadataResponse } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
||||
|
||||
type QueryResult = UseQueryResult<GetMetricMetadata200, Error>;
|
||||
type QueryResult = UseQueryResult<
|
||||
SuccessResponseV2<MetricMetadataResponse>,
|
||||
Error
|
||||
>;
|
||||
|
||||
type UseGetMultipleMetrics = (
|
||||
metricNames: string[],
|
||||
options?: UseQueryOptions<GetMetricMetadata200, Error>,
|
||||
options?: UseQueryOptions<SuccessResponseV2<MetricMetadataResponse>, Error>,
|
||||
headers?: Record<string, string>,
|
||||
) => QueryResult[];
|
||||
|
||||
export const useGetMultipleMetrics: UseGetMultipleMetrics = (
|
||||
metricNames,
|
||||
options,
|
||||
headers,
|
||||
) =>
|
||||
useQueries(
|
||||
metricNames.map(
|
||||
(metricName) =>
|
||||
({
|
||||
queryKey: getGetMetricMetadataQueryKey({
|
||||
metricName,
|
||||
}),
|
||||
queryFn: ({ signal }) =>
|
||||
getMetricMetadata(
|
||||
{
|
||||
metricName,
|
||||
},
|
||||
signal,
|
||||
),
|
||||
queryKey: [REACT_QUERY_KEY.GET_METRIC_METADATA, metricName],
|
||||
queryFn: ({ signal }) => getMetricMetadata(metricName, signal, headers),
|
||||
...options,
|
||||
} as UseQueryOptions<GetMetricMetadata200, Error>),
|
||||
} as UseQueryOptions<SuccessResponseV2<MetricMetadataResponse>, Error>),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { MetricsexplorertypesMetricMetadataDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { useGetMetrics } from 'container/MetricsExplorer/Explorer/utils';
|
||||
import { MetricMetadata } from 'types/api/metricsExplorer/v2/getMetricMetadata';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||
@@ -24,13 +24,13 @@ const mockUseGetMetrics = useGetMetrics as jest.MockedFunction<
|
||||
|
||||
const MOCK_METRIC_1 = {
|
||||
unit: UniversalYAxisUnit.BYTES,
|
||||
} as MetricsexplorertypesMetricMetadataDTO;
|
||||
} as MetricMetadata;
|
||||
const MOCK_METRIC_2 = {
|
||||
unit: UniversalYAxisUnit.SECONDS,
|
||||
} as MetricsexplorertypesMetricMetadataDTO;
|
||||
} as MetricMetadata;
|
||||
const MOCK_METRIC_3 = {
|
||||
unit: '',
|
||||
} as MetricsexplorertypesMetricMetadataDTO;
|
||||
} as MetricMetadata;
|
||||
|
||||
function createMockCurrentQuery(
|
||||
queryType: EQueryType,
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
.tooltip-plugin-container {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 1070;
|
||||
white-space: pre;
|
||||
border-radius: 4px;
|
||||
position: fixed;
|
||||
overflow: auto;
|
||||
transform: translate(-1000px, -1000px); // hide the tooltip initially
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&.pinned {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
&.visible {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,9 +339,8 @@ export default function TooltipPlugin({
|
||||
return;
|
||||
}
|
||||
const layout = layoutRef.current;
|
||||
layout.observer.disconnect();
|
||||
|
||||
if (containerRef.current) {
|
||||
layout.observer.disconnect();
|
||||
layout.observer.observe(containerRef.current);
|
||||
const { width, height } = containerRef.current.getBoundingClientRect();
|
||||
layout.width = width;
|
||||
@@ -352,28 +351,24 @@ export default function TooltipPlugin({
|
||||
}
|
||||
}, [isHovering, plot]);
|
||||
|
||||
if (!plot) {
|
||||
if (!plot || !isHovering) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={cx('tooltip-plugin-container', {
|
||||
pinned: isPinned,
|
||||
visible: isHovering,
|
||||
})}
|
||||
className={cx('tooltip-plugin-container', { pinned: isPinned })}
|
||||
style={{
|
||||
...style,
|
||||
maxWidth: `${maxWidth}px`,
|
||||
maxHeight: `${maxHeight}px`,
|
||||
width: '100%',
|
||||
}}
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
aria-hidden={!isHovering}
|
||||
ref={containerRef}
|
||||
data-testid="tooltip-plugin-container"
|
||||
>
|
||||
{isHovering ? contents : null}
|
||||
{contents}
|
||||
</div>,
|
||||
portalRoot.current,
|
||||
);
|
||||
|
||||
@@ -187,7 +187,9 @@ describe('TooltipPlugin', () => {
|
||||
canPinTooltip: true,
|
||||
});
|
||||
|
||||
const container = screen.getByTestId('tooltip-plugin-container');
|
||||
const container = document.querySelector(
|
||||
'.tooltip-plugin-container',
|
||||
) as HTMLElement;
|
||||
expect(container.classList.contains('pinned')).toBe(false);
|
||||
|
||||
act(() => {
|
||||
@@ -195,9 +197,11 @@ describe('TooltipPlugin', () => {
|
||||
});
|
||||
|
||||
return waitFor(() => {
|
||||
const updated = screen.getByTestId('tooltip-plugin-container');
|
||||
expect(updated).toBeInTheDocument();
|
||||
expect(updated.classList.contains('pinned')).toBe(true);
|
||||
const updated = document.querySelector(
|
||||
'.tooltip-plugin-container',
|
||||
) as HTMLElement | null;
|
||||
expect(updated).not.toBeNull();
|
||||
expect(updated?.classList.contains('pinned')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -245,13 +249,7 @@ describe('TooltipPlugin', () => {
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
const container = screen.getByTestId('tooltip-plugin-container');
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container.getAttribute('aria-hidden')).toBe('true');
|
||||
expect(container.classList.contains('visible')).toBe(false);
|
||||
expect(container.classList.contains('pinned')).toBe(false);
|
||||
expect(container.textContent).toBe('');
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -294,16 +292,12 @@ describe('TooltipPlugin', () => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
const container = screen.getByTestId('tooltip-plugin-container');
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container.getAttribute('aria-hidden')).toBe('true');
|
||||
expect(container.classList.contains('visible')).toBe(false);
|
||||
expect(container.classList.contains('pinned')).toBe(false);
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('unpins the tooltip on outside mousedown', async () => {
|
||||
it('unpins the tooltip on outside mousedown', () => {
|
||||
jest.useFakeTimers();
|
||||
const config = createConfigMock();
|
||||
|
||||
@@ -341,19 +335,12 @@ describe('TooltipPlugin', () => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const container = screen.getByTestId('tooltip-plugin-container');
|
||||
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container.getAttribute('aria-hidden')).toBe('true');
|
||||
expect(container.classList.contains('visible')).toBe(false);
|
||||
expect(container.classList.contains('pinned')).toBe(false);
|
||||
});
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('unpins the tooltip on outside keydown', async () => {
|
||||
it('unpins the tooltip on outside keydown', () => {
|
||||
jest.useFakeTimers();
|
||||
const config = createConfigMock();
|
||||
|
||||
@@ -393,13 +380,7 @@ describe('TooltipPlugin', () => {
|
||||
jest.runAllTimers();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const container = screen.getByTestId('tooltip-plugin-container');
|
||||
expect(container).toBeInTheDocument();
|
||||
expect(container.getAttribute('aria-hidden')).toBe('true');
|
||||
expect(container.classList.contains('visible')).toBe(false);
|
||||
expect(container.classList.contains('pinned')).toBe(false);
|
||||
});
|
||||
expect(document.querySelector('.tooltip-plugin-container')).toBeNull();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
@@ -22,9 +22,7 @@ import ROUTES from 'constants/routes';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useDashboardVariablesFromLocalStorage } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useTabVisibility from 'hooks/useTabFocus';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getUpdatedLayout } from 'lib/dashboard/getUpdatedLayout';
|
||||
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
|
||||
import { defaultTo, isEmpty } from 'lodash-es';
|
||||
@@ -50,11 +48,7 @@ import {
|
||||
setDashboardVariablesStore,
|
||||
updateDashboardVariablesStore,
|
||||
} from './store/dashboardVariables/dashboardVariablesStore';
|
||||
import {
|
||||
DashboardSortOrder,
|
||||
IDashboardContext,
|
||||
WidgetColumnWidths,
|
||||
} from './types';
|
||||
import { IDashboardContext, WidgetColumnWidths } from './types';
|
||||
import { sortLayout } from './util';
|
||||
|
||||
const DashboardContext = createContext<IDashboardContext>({
|
||||
@@ -71,13 +65,7 @@ const DashboardContext = createContext<IDashboardContext>({
|
||||
layouts: [],
|
||||
panelMap: {},
|
||||
setPanelMap: () => {},
|
||||
listSortOrder: {
|
||||
columnKey: 'createdAt',
|
||||
order: 'descend',
|
||||
pagination: '1',
|
||||
search: '',
|
||||
},
|
||||
setListSortOrder: () => {},
|
||||
|
||||
setLayouts: () => {},
|
||||
setSelectedDashboard: () => {},
|
||||
updatedTimeRef: {} as React.MutableRefObject<Dayjs | null>,
|
||||
@@ -101,7 +89,6 @@ interface Props {
|
||||
export function DashboardProvider({
|
||||
children,
|
||||
}: PropsWithChildren): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const [isDashboardSliderOpen, setIsDashboardSlider] = useState<boolean>(false);
|
||||
|
||||
const [toScrollWidgetId, setToScrollWidgetId] = useState<string>('');
|
||||
@@ -122,52 +109,8 @@ export function DashboardProvider({
|
||||
exact: true,
|
||||
});
|
||||
|
||||
const isDashboardListPage = useRouteMatch<Props>({
|
||||
path: ROUTES.ALL_DASHBOARD,
|
||||
exact: true,
|
||||
});
|
||||
|
||||
const { showErrorModal } = useErrorModal();
|
||||
|
||||
// added extra checks here in case wrong values appear use the default values rather than empty dashboards
|
||||
const supportedOrderColumnKeys = ['createdAt', 'updatedAt'];
|
||||
|
||||
const supportedOrderKeys = ['ascend', 'descend'];
|
||||
|
||||
const params = useUrlQuery();
|
||||
// since the dashboard provider is wrapped at the very top of the application hence it initialises these values from other pages as well.
|
||||
// pick the below params from URL only if the user is on the dashboards list page.
|
||||
const orderColumnParam = isDashboardListPage && params.get('columnKey');
|
||||
const orderQueryParam = isDashboardListPage && params.get('order');
|
||||
const paginationParam = isDashboardListPage && params.get('page');
|
||||
const searchParam = isDashboardListPage && params.get('search');
|
||||
|
||||
const [listSortOrder, setListOrder] = useState({
|
||||
columnKey: orderColumnParam
|
||||
? supportedOrderColumnKeys.includes(orderColumnParam)
|
||||
? orderColumnParam
|
||||
: 'updatedAt'
|
||||
: 'updatedAt',
|
||||
order: orderQueryParam
|
||||
? supportedOrderKeys.includes(orderQueryParam)
|
||||
? orderQueryParam
|
||||
: 'descend'
|
||||
: 'descend',
|
||||
pagination: paginationParam || '1',
|
||||
search: searchParam || '',
|
||||
});
|
||||
|
||||
function setListSortOrder(sortOrder: DashboardSortOrder): void {
|
||||
if (!isEqual(sortOrder, listSortOrder)) {
|
||||
setListOrder(sortOrder);
|
||||
}
|
||||
params.set('columnKey', sortOrder.columnKey as string);
|
||||
params.set('order', sortOrder.order as string);
|
||||
params.set('page', sortOrder.pagination || '1');
|
||||
params.set('search', sortOrder.search || '');
|
||||
safeNavigate({ search: params.toString() });
|
||||
}
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
@@ -502,8 +445,6 @@ export function DashboardProvider({
|
||||
selectedDashboard,
|
||||
dashboardId,
|
||||
layouts,
|
||||
listSortOrder,
|
||||
setListSortOrder,
|
||||
panelMap,
|
||||
setLayouts,
|
||||
setPanelMap,
|
||||
@@ -527,8 +468,6 @@ export function DashboardProvider({
|
||||
selectedDashboard,
|
||||
dashboardId,
|
||||
layouts,
|
||||
listSortOrder,
|
||||
setListSortOrder,
|
||||
panelMap,
|
||||
toScrollWidgetId,
|
||||
updateLocalStorageDashboardVariables,
|
||||
|
||||
@@ -4,13 +4,6 @@ import dayjs from 'dayjs';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
export interface DashboardSortOrder {
|
||||
columnKey: string;
|
||||
order: string;
|
||||
pagination: string;
|
||||
search: string;
|
||||
}
|
||||
|
||||
export type WidgetColumnWidths = {
|
||||
[widgetId: string]: Record<string, number>;
|
||||
};
|
||||
@@ -26,8 +19,6 @@ export interface IDashboardContext {
|
||||
layouts: Layout[];
|
||||
panelMap: Record<string, { widgets: Layout[]; collapsed: boolean }>;
|
||||
setPanelMap: React.Dispatch<React.SetStateAction<Record<string, any>>>;
|
||||
listSortOrder: DashboardSortOrder;
|
||||
setListSortOrder: (sortOrder: DashboardSortOrder) => void;
|
||||
setLayouts: React.Dispatch<React.SetStateAction<Layout[]>>;
|
||||
setSelectedDashboard: React.Dispatch<
|
||||
React.SetStateAction<Dashboard | undefined>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
export function pluralize(
|
||||
count: number,
|
||||
singular: string,
|
||||
plural?: string,
|
||||
): string {
|
||||
if (count === 1) {
|
||||
return `${count} ${singular}`;
|
||||
}
|
||||
if (plural) {
|
||||
return `${count} ${plural}`;
|
||||
}
|
||||
return `${count} ${singular}s`;
|
||||
}
|
||||
@@ -87,7 +87,7 @@ func (m *module) ListPromotedAndIndexedPaths(ctx context.Context) ([]promotetype
|
||||
}
|
||||
|
||||
func (m *module) listPromotedPaths(ctx context.Context) ([]string, error) {
|
||||
paths, err := m.metadataStore.GetPromotedPaths(ctx)
|
||||
paths, err := m.metadataStore.ListPromotedPaths(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -142,7 +142,7 @@ func (m *module) PromoteAndIndexPaths(
|
||||
pathsStr = append(pathsStr, path.Path)
|
||||
}
|
||||
|
||||
existingPromotedPaths, err := m.metadataStore.GetPromotedPaths(ctx, pathsStr...)
|
||||
existingPromotedPaths, err := m.metadataStore.ListPromotedPaths(ctx, pathsStr...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -22,8 +22,7 @@ type RootConfig struct {
|
||||
}
|
||||
|
||||
type OrgConfig struct {
|
||||
ID valuer.UUID `mapstructure:"id"`
|
||||
Name string `mapstructure:"name"`
|
||||
Name string `mapstructure:"name"`
|
||||
}
|
||||
|
||||
type PasswordConfig struct {
|
||||
|
||||
@@ -78,43 +78,6 @@ func (s *service) Stop(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (s *service) reconcile(ctx context.Context) error {
|
||||
if !s.config.Org.ID.IsZero() {
|
||||
return s.reconcileWithOrgID(ctx)
|
||||
}
|
||||
|
||||
return s.reconcileByName(ctx)
|
||||
}
|
||||
|
||||
func (s *service) reconcileWithOrgID(ctx context.Context) error {
|
||||
org, err := s.orgGetter.Get(ctx, s.config.Org.ID)
|
||||
if err != nil {
|
||||
if !errors.Ast(err, errors.TypeNotFound) {
|
||||
return err // something really went wrong
|
||||
}
|
||||
|
||||
// org was not found using id check if we can find an org using name
|
||||
|
||||
existingOrgByName, nameErr := s.orgGetter.GetByName(ctx, s.config.Org.Name)
|
||||
if nameErr != nil && !errors.Ast(nameErr, errors.TypeNotFound) {
|
||||
return nameErr // something really went wrong
|
||||
}
|
||||
|
||||
// we found an org using name
|
||||
if existingOrgByName != nil {
|
||||
// the existing org has the same name as config but org id is different inform user with actionable message
|
||||
return errors.Newf(errors.TypeInvalidInput, errors.CodeInvalidInput, "organization with name %q already exists with a different ID %s (expected %s)", s.config.Org.Name, existingOrgByName.ID.StringValue(), s.config.Org.ID.StringValue())
|
||||
}
|
||||
|
||||
// default - we did not found any org using id and name both - create a new org
|
||||
newOrg := types.NewOrganizationWithID(s.config.Org.ID, s.config.Org.Name, s.config.Org.Name)
|
||||
_, err = s.module.CreateFirstUser(ctx, newOrg, s.config.Email.String(), s.config.Email, s.config.Password)
|
||||
return err
|
||||
}
|
||||
|
||||
return s.reconcileRootUser(ctx, org.ID)
|
||||
}
|
||||
|
||||
func (s *service) reconcileByName(ctx context.Context) error {
|
||||
org, err := s.orgGetter.GetByName(ctx, s.config.Org.Name)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeNotFound) {
|
||||
|
||||
@@ -80,16 +80,11 @@ func (q *builderQuery[T]) Fingerprint() string {
|
||||
case qbtypes.LogAggregation:
|
||||
aggParts = append(aggParts, a.Expression)
|
||||
case qbtypes.MetricAggregation:
|
||||
var spaceAggParamStr string
|
||||
if a.ComparisonSpaceAggregationParam != nil {
|
||||
spaceAggParamStr = a.ComparisonSpaceAggregationParam.StringValue()
|
||||
}
|
||||
aggParts = append(aggParts, fmt.Sprintf("%s:%s:%s:%s:%s",
|
||||
aggParts = append(aggParts, fmt.Sprintf("%s:%s:%s:%s",
|
||||
a.MetricName,
|
||||
a.Temporality.StringValue(),
|
||||
a.TimeAggregation.StringValue(),
|
||||
a.SpaceAggregation.StringValue(),
|
||||
spaceAggParamStr,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,17 +276,15 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
|
||||
// Fetch temporality for all metrics at once
|
||||
var metricTemporality map[string]metrictypes.Temporality
|
||||
var metricTypes map[string]metrictypes.Type
|
||||
if len(metricNames) > 0 {
|
||||
var err error
|
||||
metricTemporality, metricTypes, err = q.metadataStore.FetchTemporalityAndTypeMulti(ctx, req.Start, req.End, metricNames...)
|
||||
metricTemporality, err = q.metadataStore.FetchTemporalityMulti(ctx, req.Start, req.End, metricNames...)
|
||||
if err != nil {
|
||||
q.logger.WarnContext(ctx, "failed to fetch metric temporality", "error", err, "metrics", metricNames)
|
||||
// Continue without temporality - statement builder will handle unspecified
|
||||
metricTemporality = make(map[string]metrictypes.Temporality)
|
||||
metricTypes = make(map[string]metrictypes.Type)
|
||||
}
|
||||
q.logger.DebugContext(ctx, "fetched metric temporalities and types", "metric_temporality", metricTemporality, "metric_types", metricTypes)
|
||||
q.logger.DebugContext(ctx, "fetched metric temporalities", "metric_temporality", metricTemporality)
|
||||
}
|
||||
|
||||
queries := make(map[string]qbtypes.Query)
|
||||
@@ -382,12 +380,6 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
if spec.Aggregations[i].Temporality == metrictypes.Unknown {
|
||||
spec.Aggregations[i].Temporality = metrictypes.Unspecified
|
||||
}
|
||||
|
||||
if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Type == metrictypes.UnspecifiedType {
|
||||
if foundMetricType, ok := metricTypes[spec.Aggregations[i].MetricName]; ok && foundMetricType != metrictypes.UnspecifiedType {
|
||||
spec.Aggregations[i].Type = foundMetricType
|
||||
}
|
||||
}
|
||||
}
|
||||
spec.ShiftBy = extractShiftFromBuilderQuery(spec)
|
||||
timeRange := adjustTimeRangeForShift(spec, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType)
|
||||
|
||||
@@ -4308,28 +4308,6 @@ func (r *ClickHouseReader) GetListResultV3(ctx context.Context, query string) ([
|
||||
|
||||
}
|
||||
|
||||
// GetHostMetricsExistenceAndEarliestTime returns (count, minFirstReportedUnixMilli, error) for the given host metric names
|
||||
// from distributed_metadata. When count is 0, minFirstReportedUnixMilli is 0.
|
||||
func (r *ClickHouseReader) GetMetricsExistenceAndEarliestTime(ctx context.Context, metricNames []string) (uint64, uint64, error) {
|
||||
if len(metricNames) == 0 {
|
||||
return 0, 0, nil
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(
|
||||
`SELECT count(*) AS cnt, min(first_reported_unix_milli) AS min_first_reported
|
||||
FROM %s.%s
|
||||
WHERE metric_name IN @metric_names`,
|
||||
constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_METADATA_TABLENAME)
|
||||
|
||||
var count, minFirstReported uint64
|
||||
err := r.db.QueryRow(ctx, query, clickhouse.Named("metric_names", metricNames)).Scan(&count, &minFirstReported)
|
||||
if err != nil {
|
||||
zap.L().Error("error getting host metrics existence and earliest time", zap.Error(err))
|
||||
return 0, 0, err
|
||||
}
|
||||
return count, minFirstReported, nil
|
||||
}
|
||||
|
||||
func getPersonalisedError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
)
|
||||
|
||||
var dotMetricMap = map[string]string{
|
||||
"system_filesystem_usage": "system.filesystem.usage",
|
||||
"system_cpu_time": "system.cpu.time",
|
||||
"system_memory_usage": "system.memory.usage",
|
||||
"system_cpu_load_average_15m": "system.cpu.load_average.15m",
|
||||
|
||||
@@ -67,11 +67,10 @@ var (
|
||||
GetDotMetrics("os_type"),
|
||||
}
|
||||
metricNamesForHosts = map[string]string{
|
||||
"filesystem": GetDotMetrics("system_filesystem_usage"),
|
||||
"cpu": GetDotMetrics("system_cpu_time"),
|
||||
"memory": GetDotMetrics("system_memory_usage"),
|
||||
"load15": GetDotMetrics("system_cpu_load_average_15m"),
|
||||
"wait": GetDotMetrics("system_cpu_time"),
|
||||
"cpu": GetDotMetrics("system_cpu_time"),
|
||||
"memory": GetDotMetrics("system_memory_usage"),
|
||||
"load15": GetDotMetrics("system_cpu_load_average_15m"),
|
||||
"wait": GetDotMetrics("system_cpu_time"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -317,15 +316,24 @@ func (h *HostsRepo) getTopHostGroups(ctx context.Context, orgID valuer.UUID, req
|
||||
return topHostGroups, allHostGroups, nil
|
||||
}
|
||||
|
||||
// GetHostMetricsExistenceAndEarliestTime returns (count, minFirstReportedUnixMilli, error) for host metrics
|
||||
// in distributed_metadata. Uses metricNamesForHosts plus system.filesystem.usage.
|
||||
func (h *HostsRepo) GetHostMetricsExistenceAndEarliestTime(ctx context.Context, req model.HostListRequest) (uint64, uint64, error) {
|
||||
func (h *HostsRepo) DidSendHostMetricsData(ctx context.Context, req model.HostListRequest) (bool, error) {
|
||||
|
||||
names := []string{}
|
||||
for _, metricName := range metricNamesForHosts {
|
||||
names = append(names, metricName)
|
||||
}
|
||||
|
||||
return h.reader.GetMetricsExistenceAndEarliestTime(ctx, names)
|
||||
namesStr := "'" + strings.Join(names, "','") + "'"
|
||||
|
||||
query := fmt.Sprintf("SELECT count() FROM %s.%s WHERE metric_name IN (%s)",
|
||||
constants.SIGNOZ_METRIC_DBNAME, constants.SIGNOZ_TIMESERIES_v4_1DAY_TABLENAME, namesStr)
|
||||
|
||||
count, err := h.reader.GetCountOfThings(ctx, query)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (h *HostsRepo) IsSendingK8SAgentMetrics(ctx context.Context, req model.HostListRequest) ([]string, []string, error) {
|
||||
@@ -404,25 +412,8 @@ func (h *HostsRepo) GetHostList(ctx context.Context, orgID valuer.UUID, req mode
|
||||
resp.ClusterNames = clusterNames
|
||||
resp.NodeNames = nodeNames
|
||||
}
|
||||
|
||||
// 1. Check if any host metrics exist and get earliest retention time
|
||||
// if no hosts metrics exist, that means we should show the onboarding guide on UI, and return early.
|
||||
// 2. If host metrics exist, but req.End is earlier than the earliest time of host metrics as read from
|
||||
// metadata table, then we should convey the same to the user and return early
|
||||
if count, minFirstReportedUnixMilli, err := h.GetHostMetricsExistenceAndEarliestTime(ctx, req); err == nil {
|
||||
if count == 0 {
|
||||
resp.SentAnyHostMetricsData = false
|
||||
resp.Records = []model.HostListRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
resp.SentAnyHostMetricsData = true
|
||||
if req.End < int64(minFirstReportedUnixMilli) {
|
||||
resp.EndTimeBeforeRetention = true
|
||||
resp.Records = []model.HostListRecord{}
|
||||
resp.Total = 0
|
||||
return resp, nil
|
||||
}
|
||||
if sentAnyHostMetricsData, err := h.DidSendHostMetricsData(ctx, req); err == nil {
|
||||
resp.SentAnyHostMetricsData = sentAnyHostMetricsData
|
||||
}
|
||||
|
||||
step := int64(math.Max(float64(common.MinAllowedStepInterval(req.Start, req.End)), 60))
|
||||
|
||||
@@ -125,8 +125,6 @@ const (
|
||||
SIGNOZ_TIMESERIES_v4_6HRS_TABLENAME = "distributed_time_series_v4_6hrs"
|
||||
SIGNOZ_ATTRIBUTES_METADATA_TABLENAME = "distributed_attributes_metadata"
|
||||
SIGNOZ_ATTRIBUTES_METADATA_LOCAL_TABLENAME = "attributes_metadata"
|
||||
SIGNOZ_METADATA_TABLENAME = "distributed_metadata"
|
||||
SIGNOZ_METADATA_LOCAL_TABLENAME = "metadata"
|
||||
)
|
||||
|
||||
// alert related constants
|
||||
|
||||
@@ -100,8 +100,6 @@ type Reader interface {
|
||||
|
||||
GetCountOfThings(ctx context.Context, query string) (uint64, error)
|
||||
|
||||
GetMetricsExistenceAndEarliestTime(ctx context.Context, metricNames []string) (uint64, uint64, error)
|
||||
|
||||
//trace
|
||||
GetTraceFields(ctx context.Context) (*model.GetFieldsResponse, *model.ApiError)
|
||||
UpdateTraceField(ctx context.Context, field *model.UpdateField) *model.ApiError
|
||||
|
||||
@@ -44,7 +44,6 @@ type HostListResponse struct {
|
||||
IsSendingK8SAgentMetrics bool `json:"isSendingK8SAgentMetrics"`
|
||||
ClusterNames []string `json:"clusterNames"`
|
||||
NodeNames []string `json:"nodeNames"`
|
||||
EndTimeBeforeRetention bool `json:"endTimeBeforeRetention"`
|
||||
}
|
||||
|
||||
func (r *HostListResponse) SortBy(orderBy *v3.OrderBy) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/ClickHouse/clickhouse-go/v2/lib/chcol"
|
||||
schemamigrator "github.com/SigNoz/signoz-otel-collector/cmd/signozschemamigrator/schema_migrator"
|
||||
"github.com/SigNoz/signoz-otel-collector/constants"
|
||||
"github.com/SigNoz/signoz-otel-collector/utils"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrylogs"
|
||||
@@ -112,7 +113,7 @@ func (t *telemetryMetaStore) buildBodyJSONPaths(ctx context.Context,
|
||||
|
||||
for _, fieldKey := range fieldKeys {
|
||||
promotedKey := strings.Split(fieldKey.Name, telemetrytypes.ArraySep)[0]
|
||||
fieldKey.Materialized = promoted[promotedKey]
|
||||
fieldKey.Materialized = promoted.Contains(promotedKey)
|
||||
fieldKey.Indexes = indexes[fieldKey.Name]
|
||||
}
|
||||
|
||||
@@ -294,6 +295,33 @@ func (t *telemetryMetaStore) ListLogsJSONIndexes(ctx context.Context, filters ..
|
||||
return indexes, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) ListPromotedPaths(ctx context.Context, paths ...string) (map[string]struct{}, error) {
|
||||
sb := sqlbuilder.Select("path").From(fmt.Sprintf("%s.%s", DBName, PromotedPathsTableName))
|
||||
pathConditions := []string{}
|
||||
for _, path := range paths {
|
||||
pathConditions = append(pathConditions, sb.Equal("path", path))
|
||||
}
|
||||
sb.Where(sb.Or(pathConditions...))
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
|
||||
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.WrapInternalf(err, CodeFailLoadPromotedPaths, "failed to load promoted paths")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
next := make(map[string]struct{})
|
||||
for rows.Next() {
|
||||
var path string
|
||||
if err := rows.Scan(&path); err != nil {
|
||||
return nil, errors.WrapInternalf(err, CodeFailLoadPromotedPaths, "failed to scan promoted path")
|
||||
}
|
||||
next[path] = struct{}{}
|
||||
}
|
||||
|
||||
return next, nil
|
||||
}
|
||||
|
||||
// TODO(Piyush): Remove this if not used in future
|
||||
func (t *telemetryMetaStore) ListJSONValues(ctx context.Context, path string, limit int) (*telemetrytypes.TelemetryFieldValues, bool, error) {
|
||||
path = CleanPathPrefixes(path)
|
||||
@@ -456,12 +484,11 @@ func derefValue(v any) any {
|
||||
return val.Interface()
|
||||
}
|
||||
|
||||
// IsPathPromoted checks if a specific path is promoted (Column Evolution table: field_name for logs body).
|
||||
// IsPathPromoted checks if a specific path is promoted
|
||||
func (t *telemetryMetaStore) IsPathPromoted(ctx context.Context, path string) (bool, error) {
|
||||
split := strings.Split(path, telemetrytypes.ArraySep)
|
||||
pathSegment := split[0]
|
||||
query := fmt.Sprintf("SELECT 1 FROM %s.%s WHERE signal = ? AND column_name = ? AND field_context = ? AND field_name = ? LIMIT 1", DBName, PromotedPathsTableName)
|
||||
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, telemetrytypes.SignalLogs, telemetrylogs.LogsV2BodyPromotedColumn, telemetrytypes.FieldContextBody, pathSegment)
|
||||
query := fmt.Sprintf("SELECT 1 FROM %s.%s WHERE path = ? LIMIT 1", DBName, PromotedPathsTableName)
|
||||
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, split[0])
|
||||
if err != nil {
|
||||
return false, errors.WrapInternalf(err, CodeFailCheckPathPromoted, "failed to check if path %s is promoted", path)
|
||||
}
|
||||
@@ -470,24 +497,15 @@ func (t *telemetryMetaStore) IsPathPromoted(ctx context.Context, path string) (b
|
||||
return rows.Next(), nil
|
||||
}
|
||||
|
||||
// GetPromotedPaths returns promoted paths from the Column Evolution table (field_name for logs body).
|
||||
func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...string) (map[string]bool, error) {
|
||||
sb := sqlbuilder.Select("field_name").From(fmt.Sprintf("%s.%s", DBName, PromotedPathsTableName))
|
||||
conditions := []string{
|
||||
sb.Equal("signal", telemetrytypes.SignalLogs),
|
||||
sb.Equal("column_name", telemetrylogs.LogsV2BodyPromotedColumn),
|
||||
sb.Equal("field_context", telemetrytypes.FieldContextBody),
|
||||
sb.NotEqual("field_name", "__all__"),
|
||||
// GetPromotedPaths checks if a specific path is promoted
|
||||
func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...string) (*utils.ConcurrentSet[string], error) {
|
||||
sb := sqlbuilder.Select("path").From(fmt.Sprintf("%s.%s", DBName, PromotedPathsTableName))
|
||||
pathConditions := []string{}
|
||||
for _, path := range paths {
|
||||
split := strings.Split(path, telemetrytypes.ArraySep)
|
||||
pathConditions = append(pathConditions, sb.Equal("path", split[0]))
|
||||
}
|
||||
if len(paths) > 0 {
|
||||
pathArgs := make([]interface{}, len(paths))
|
||||
for i, path := range paths {
|
||||
split := strings.Split(path, telemetrytypes.ArraySep)
|
||||
pathArgs[i] = split[0]
|
||||
}
|
||||
conditions = append(conditions, sb.In("field_name", pathArgs))
|
||||
}
|
||||
sb.Where(sb.And(conditions...))
|
||||
sb.Where(sb.Or(pathConditions...))
|
||||
|
||||
query, args := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
|
||||
@@ -496,13 +514,13 @@ func (t *telemetryMetaStore) GetPromotedPaths(ctx context.Context, paths ...stri
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
promotedPaths := make(map[string]bool)
|
||||
promotedPaths := utils.NewConcurrentSet[string]()
|
||||
for rows.Next() {
|
||||
var fieldName string
|
||||
if err := rows.Scan(&fieldName); err != nil {
|
||||
var path string
|
||||
if err := rows.Scan(&path); err != nil {
|
||||
return nil, errors.WrapInternalf(err, CodeFailCheckPathPromoted, "failed to scan promoted path")
|
||||
}
|
||||
promotedPaths[fieldName] = true
|
||||
promotedPaths.Insert(path)
|
||||
}
|
||||
|
||||
return promotedPaths, nil
|
||||
@@ -516,22 +534,21 @@ func CleanPathPrefixes(path string) string {
|
||||
return path
|
||||
}
|
||||
|
||||
// PromotePaths inserts promoted paths into the Column Evolution table (same schema as signoz-otel-collector metadata_migrations).
|
||||
func (t *telemetryMetaStore) PromotePaths(ctx context.Context, paths ...string) error {
|
||||
batch, err := t.telemetrystore.ClickhouseDB().PrepareBatch(ctx,
|
||||
fmt.Sprintf("INSERT INTO %s.%s (signal, column_name, column_type, field_context, field_name, version, release_time) VALUES", DBName,
|
||||
fmt.Sprintf("INSERT INTO %s.%s (path, created_at) VALUES", DBName,
|
||||
PromotedPathsTableName))
|
||||
if err != nil {
|
||||
return errors.WrapInternalf(err, CodeFailedToPrepareBatch, "failed to prepare batch")
|
||||
}
|
||||
|
||||
releaseTime := time.Now().UnixNano()
|
||||
nowMs := uint64(time.Now().UnixMilli())
|
||||
for _, p := range paths {
|
||||
trimmed := strings.TrimSpace(p)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if err := batch.Append(telemetrytypes.SignalLogs, telemetrylogs.LogsV2BodyPromotedColumn, "JSON()", telemetrytypes.FieldContextBody, trimmed, 0, releaseTime); err != nil {
|
||||
if err := batch.Append(trimmed, nowMs); err != nil {
|
||||
_ = batch.Abort()
|
||||
return errors.WrapInternalf(err, CodeFailedToAppendPath, "failed to append path")
|
||||
}
|
||||
|
||||
@@ -1616,52 +1616,40 @@ func (t *telemetryMetaStore) FetchTemporality(ctx context.Context, queryTimeRang
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) FetchTemporalityMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, error) {
|
||||
temporalities, _, err := t.FetchTemporalityAndTypeMulti(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricNames...)
|
||||
return temporalities, err
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) FetchTemporalityAndTypeMulti(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error) {
|
||||
if len(metricNames) == 0 {
|
||||
return make(map[string]metrictypes.Temporality), make(map[string]metrictypes.Type), nil
|
||||
return make(map[string]metrictypes.Temporality), nil
|
||||
}
|
||||
|
||||
temporalities := make(map[string]metrictypes.Temporality)
|
||||
types := make(map[string]metrictypes.Type)
|
||||
metricsTemporality, metricTypes, err := t.fetchMetricsTemporalityAndType(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricNames...)
|
||||
result := make(map[string]metrictypes.Temporality)
|
||||
metricsTemporality, err := t.fetchMetricsTemporality(ctx, queryTimeRangeStartTs, queryTimeRangeEndTs, metricNames...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
// TODO: return error after table migration are run
|
||||
meterMetricsTemporality, meterMetricsTypes, _ := t.fetchMeterSourceMetricsTemporalityAndType(ctx, metricNames...)
|
||||
meterMetricsTemporality, _ := t.fetchMeterSourceMetricsTemporality(ctx, metricNames...)
|
||||
|
||||
// For metrics not found in the database, set to Unknown
|
||||
for _, metricName := range metricNames {
|
||||
if temporality, exists := metricsTemporality[metricName]; exists && len(temporality) > 0 {
|
||||
if len(temporality) > 1 {
|
||||
temporalities[metricName] = metrictypes.Multiple
|
||||
result[metricName] = metrictypes.Multiple
|
||||
} else {
|
||||
temporalities[metricName] = temporality[0]
|
||||
result[metricName] = temporality[0]
|
||||
}
|
||||
} else if temporality, exists := meterMetricsTemporality[metricName]; exists {
|
||||
temporalities[metricName] = temporality
|
||||
} else {
|
||||
temporalities[metricName] = metrictypes.Unknown
|
||||
continue
|
||||
}
|
||||
if metricType, exists := metricTypes[metricName]; exists {
|
||||
types[metricName] = metricType
|
||||
} else if meterMetricType, exists := meterMetricsTypes[metricName]; exists {
|
||||
types[metricName] = meterMetricType
|
||||
} else {
|
||||
types[metricName] = metrictypes.UnspecifiedType
|
||||
if temporality, exists := meterMetricsTemporality[metricName]; exists {
|
||||
result[metricName] = temporality
|
||||
continue
|
||||
}
|
||||
result[metricName] = metrictypes.Unknown
|
||||
}
|
||||
|
||||
return temporalities, types, nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) fetchMetricsTemporalityAndType(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string][]metrictypes.Temporality, map[string]metrictypes.Type, error) {
|
||||
temporalities := make(map[string][]metrictypes.Temporality)
|
||||
types := make(map[string]metrictypes.Type)
|
||||
func (t *telemetryMetaStore) fetchMetricsTemporality(ctx context.Context, queryTimeRangeStartTs, queryTimeRangeEndTs uint64, metricNames ...string) (map[string][]metrictypes.Temporality, error) {
|
||||
result := make(map[string][]metrictypes.Temporality)
|
||||
|
||||
adjustedStartTs, adjustedEndTs, tsTableName, _ := telemetrymetrics.WhichTSTableToUse(queryTimeRangeStartTs, queryTimeRangeEndTs, nil)
|
||||
|
||||
@@ -1672,8 +1660,6 @@ func (t *telemetryMetaStore) fetchMetricsTemporalityAndType(ctx context.Context,
|
||||
sb := sqlbuilder.Select(
|
||||
"metric_name",
|
||||
"temporality",
|
||||
"any(type) AS type",
|
||||
"any(is_monotonic) as is_monotonic",
|
||||
).
|
||||
From(t.metricsDBName + "." + tsTableName)
|
||||
|
||||
@@ -1692,42 +1678,47 @@ func (t *telemetryMetaStore) fetchMetricsTemporalityAndType(ctx context.Context,
|
||||
|
||||
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch metric temporality")
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch metric temporality")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Process results
|
||||
for rows.Next() {
|
||||
var metricName string
|
||||
var metricName, temporalityStr string
|
||||
if err := rows.Scan(&metricName, &temporalityStr); err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
|
||||
}
|
||||
|
||||
// Convert string to Temporality type
|
||||
var temporality metrictypes.Temporality
|
||||
var metricType metrictypes.Type
|
||||
var isMonotonic bool
|
||||
if err := rows.Scan(&metricName, &temporality, &metricType, &isMonotonic); err != nil {
|
||||
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
|
||||
switch temporalityStr {
|
||||
case "Delta":
|
||||
temporality = metrictypes.Delta
|
||||
case "Cumulative":
|
||||
temporality = metrictypes.Cumulative
|
||||
case "Unspecified":
|
||||
temporality = metrictypes.Unspecified
|
||||
default:
|
||||
// Unknown or empty temporality
|
||||
temporality = metrictypes.Unknown
|
||||
}
|
||||
if temporality != metrictypes.Unknown {
|
||||
temporalities[metricName] = append(temporalities[metricName], temporality)
|
||||
result[metricName] = append(result[metricName], temporality)
|
||||
}
|
||||
if metricType == metrictypes.SumType && !isMonotonic {
|
||||
metricType = metrictypes.GaugeType
|
||||
}
|
||||
types[metricName] = metricType
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error iterating over metrics temporality rows")
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "error iterating over metrics temporality rows")
|
||||
}
|
||||
|
||||
return temporalities, types, nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporalityAndType(ctx context.Context, metricNames ...string) (map[string]metrictypes.Temporality, map[string]metrictypes.Type, error) {
|
||||
temporalities := make(map[string]metrictypes.Temporality)
|
||||
types := make(map[string]metrictypes.Type)
|
||||
func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporality(ctx context.Context, metricNames ...string) (map[string]metrictypes.Temporality, error) {
|
||||
result := make(map[string]metrictypes.Temporality)
|
||||
|
||||
sb := sqlbuilder.Select(
|
||||
"metric_name",
|
||||
"argMax(temporality, unix_milli) as temporality",
|
||||
"any(type) AS type",
|
||||
).From(t.meterDBName + "." + t.meterFieldsTblName)
|
||||
|
||||
// Filter by metric names (in the temporality column due to data mix-up)
|
||||
@@ -1742,27 +1733,35 @@ func (t *telemetryMetaStore) fetchMeterSourceMetricsTemporalityAndType(ctx conte
|
||||
|
||||
rows, err := t.telemetrystore.ClickhouseDB().Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch meter metric temporality")
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to fetch meter metric temporality")
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
// Process results
|
||||
for rows.Next() {
|
||||
var metricName string
|
||||
var metricName, temporalityStr string
|
||||
if err := rows.Scan(&metricName, &temporalityStr); err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
|
||||
}
|
||||
|
||||
// Convert string to Temporality type
|
||||
var temporality metrictypes.Temporality
|
||||
var metricType metrictypes.Type
|
||||
var isMonotonic bool
|
||||
if err := rows.Scan(&metricName, &temporality, &metricType, &isMonotonic); err != nil {
|
||||
return nil, nil, errors.Wrapf(err, errors.TypeInternal, errors.CodeInternal, "failed to scan temporality result")
|
||||
switch temporalityStr {
|
||||
case "Delta":
|
||||
temporality = metrictypes.Delta
|
||||
case "Cumulative":
|
||||
temporality = metrictypes.Cumulative
|
||||
case "Unspecified":
|
||||
temporality = metrictypes.Unspecified
|
||||
default:
|
||||
// Unknown or empty temporality
|
||||
temporality = metrictypes.Unknown
|
||||
}
|
||||
if metricType == metrictypes.SumType && !isMonotonic {
|
||||
metricType = metrictypes.GaugeType
|
||||
}
|
||||
temporalities[metricName] = temporality
|
||||
types[metricName] = metricType
|
||||
|
||||
result[metricName] = temporality
|
||||
}
|
||||
|
||||
return temporalities, types, nil
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// chunkSizeFirstSeenMetricMetadata limits the number of tuples per SQL query to avoid hitting the max_query_size limit.
|
||||
|
||||
@@ -7,7 +7,6 @@ const (
|
||||
AttributesMetadataTableName = "distributed_attributes_metadata"
|
||||
AttributesMetadataLocalTableName = "attributes_metadata"
|
||||
PathTypesTableName = otelcollectorconst.DistributedPathTypesTable
|
||||
// Column Evolution table stores promoted paths as (signal, column_name, field_context, field_name); see signoz-otel-collector metadata_migrations.
|
||||
PromotedPathsTableName = "distributed_column_evolution_metadata"
|
||||
PromotedPathsTableName = otelcollectorconst.DistributedPromotedPathsTable
|
||||
SkipIndexTableName = "system.data_skipping_indices"
|
||||
)
|
||||
|
||||
@@ -123,7 +123,8 @@ func (b *MetricQueryStatementBuilder) buildPipelineStatement(
|
||||
origTimeAgg := query.Aggregations[0].TimeAggregation
|
||||
origGroupBy := slices.Clone(query.GroupBy)
|
||||
|
||||
if query.Aggregations[0].Type == metrictypes.HistogramType {
|
||||
if query.Aggregations[0].SpaceAggregation.IsPercentile() &&
|
||||
query.Aggregations[0].Type != metrictypes.ExpHistogramType {
|
||||
// add le in the group by if doesn't exist
|
||||
leExists := false
|
||||
for _, g := range query.GroupBy {
|
||||
@@ -153,11 +154,7 @@ func (b *MetricQueryStatementBuilder) buildPipelineStatement(
|
||||
}
|
||||
|
||||
// make the time aggregation rate and space aggregation sum
|
||||
if query.Aggregations[0].SpaceAggregation.IsPercentile() {
|
||||
query.Aggregations[0].TimeAggregation = metrictypes.TimeAggregationRate
|
||||
} else {
|
||||
query.Aggregations[0].TimeAggregation = metrictypes.TimeAggregationIncrease
|
||||
}
|
||||
query.Aggregations[0].TimeAggregation = metrictypes.TimeAggregationRate
|
||||
query.Aggregations[0].SpaceAggregation = metrictypes.SpaceAggregationSum
|
||||
}
|
||||
|
||||
@@ -554,9 +551,6 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
|
||||
cteArgs [][]any,
|
||||
query qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation],
|
||||
) (*qbtypes.Statement, error) {
|
||||
metricType := query.Aggregations[0].Type
|
||||
spaceAgg := query.Aggregations[0].SpaceAggregation
|
||||
|
||||
combined := querybuilder.CombineCTEs(cteFragments)
|
||||
|
||||
var args []any
|
||||
@@ -566,8 +560,12 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
|
||||
|
||||
sb := sqlbuilder.NewSelectBuilder()
|
||||
|
||||
if metricType == metrictypes.HistogramType && spaceAgg.IsPercentile() {
|
||||
quantile := query.Aggregations[0].SpaceAggregation.Percentile()
|
||||
var quantile float64
|
||||
if query.Aggregations[0].SpaceAggregation.IsPercentile() {
|
||||
quantile = query.Aggregations[0].SpaceAggregation.Percentile()
|
||||
}
|
||||
|
||||
if quantile != 0 && query.Aggregations[0].Type != metrictypes.ExpHistogramType {
|
||||
sb.Select("ts")
|
||||
for _, g := range query.GroupBy {
|
||||
sb.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
|
||||
@@ -579,36 +577,12 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
|
||||
sb.From("__spatial_aggregation_cte")
|
||||
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
||||
sb.GroupBy("ts")
|
||||
if query.Having != nil && query.Having.Expression != "" {
|
||||
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||
rewrittenExpr := rewriter.RewriteForMetrics(query.Having.Expression, query.Aggregations)
|
||||
sb.Having(rewrittenExpr)
|
||||
}
|
||||
} else if metricType == metrictypes.HistogramType && spaceAgg == metrictypes.SpaceAggregationCount && query.Aggregations[0].ComparisonSpaceAggregationParam != nil {
|
||||
sb.Select("ts")
|
||||
|
||||
for _, g := range query.GroupBy {
|
||||
sb.SelectMore(fmt.Sprintf("`%s`", g.TelemetryFieldKey.Name))
|
||||
}
|
||||
|
||||
aggQuery, err := AggregationQueryForHistogramCountWithParams(query.Aggregations[0].ComparisonSpaceAggregationParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sb.SelectMore(aggQuery)
|
||||
|
||||
sb.From("__spatial_aggregation_cte")
|
||||
|
||||
sb.GroupBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
||||
sb.GroupBy("ts")
|
||||
|
||||
if query.Having != nil && query.Having.Expression != "" {
|
||||
rewriter := querybuilder.NewHavingExpressionRewriter()
|
||||
rewrittenExpr := rewriter.RewriteForMetrics(query.Having.Expression, query.Aggregations)
|
||||
sb.Having(rewrittenExpr)
|
||||
}
|
||||
} else {
|
||||
// for count aggregation on histograms with no params, the exact result of spatial aggregation can be sent forward
|
||||
sb.Select("*")
|
||||
sb.From("__spatial_aggregation_cte")
|
||||
if query.Having != nil && query.Having.Expression != "" {
|
||||
@@ -619,9 +593,6 @@ func (b *MetricQueryStatementBuilder) BuildFinalSelect(
|
||||
}
|
||||
sb.OrderBy(querybuilder.GroupByKeys(query.GroupBy)...)
|
||||
sb.OrderBy("ts")
|
||||
if metricType == metrictypes.HistogramType && spaceAgg == metrictypes.SpaceAggregationCount && query.Aggregations[0].ComparisonSpaceAggregationParam == nil {
|
||||
sb.OrderBy("toFloat64(le)")
|
||||
}
|
||||
|
||||
q, a := sb.BuildWithFlavor(sqlbuilder.ClickHouse)
|
||||
return &qbtypes.Statement{Query: combined + q, Args: append(args, a...)}, nil
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package telemetrymetrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -309,20 +308,3 @@ func AggregationColumnForSamplesTable(
|
||||
}
|
||||
return aggregationColumn, nil
|
||||
}
|
||||
|
||||
func AggregationQueryForHistogramCountWithParams(param *metrictypes.ComparisonSpaceAggregationParam) (string, error) {
|
||||
if param == nil {
|
||||
return "", errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "no aggregation param provided for histogram count")
|
||||
}
|
||||
histogramCountThreshold := param.Threshold
|
||||
|
||||
switch param.Operater {
|
||||
case "<=":
|
||||
return fmt.Sprintf("argMaxIf(value, toFloat64(le), toFloat64(le) <= %f) + (argMinIf(value, toFloat64(le), toFloat64(le) > %f) - argMaxIf(value, toFloat64(le), toFloat64(le) <= %f)) * (%f - maxIf(toFloat64(le), toFloat64(le) <= %f)) / (minIf(toFloat64(le), toFloat64(le) > %f) - maxIf(toFloat64(le), toFloat64(le) <= %f)) AS value", histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold), nil
|
||||
case ">":
|
||||
return fmt.Sprintf("argMax(value, toFloat64(le)) - (argMaxIf(value, toFloat64(le), toFloat64(le) <= %f) + (argMinIf(value, toFloat64(le), toFloat64(le) > %f) - argMaxIf(value, toFloat64(le), toFloat64(le) <= %f)) * (%f - maxIf(toFloat64(le), toFloat64(le) <= %f)) / (minIf(toFloat64(le), toFloat64(le) > %f) - maxIf(toFloat64(le), toFloat64(le) <= %f))) AS value", histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold, histogramCountThreshold), nil
|
||||
default:
|
||||
return "", errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "invalid space aggregation operator, should be one of the following: [`<=`, `>`]")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package metrictypes
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
@@ -257,12 +256,3 @@ type MetricTableHints struct {
|
||||
type MetricValueFilter struct {
|
||||
Value float64
|
||||
}
|
||||
|
||||
type ComparisonSpaceAggregationParam struct {
|
||||
Operater string `json:"operator" required:"true"`
|
||||
Threshold float64 `json:"threshold" required:"true"`
|
||||
}
|
||||
|
||||
func (param ComparisonSpaceAggregationParam) StringValue() string {
|
||||
return fmt.Sprintf("operator=%s:threshold=%f", param.Operater, param.Threshold)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user