Compare commits

..

11 Commits

Author SHA1 Message Date
Amlan Kumar Nandy
a83c477a00 Merge branch 'main' into SIGNOZ-8704 2026-02-27 07:32:11 +07:00
amlannandy
d0748314e1 chore: add unit test 2026-02-27 07:23:45 +07:00
Srikanth Chekuri
43933f3a33 chore: move converter/formatter to pkg/units/... (#10408)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
2026-02-26 23:52:58 +05:30
Naman Verma
d2e1a24b20 chore: choose latest seen unit for metrics instead of any unit (#10431) 2026-02-26 16:48:22 +00:00
amlannandy
5bbe27946d chore: address comments 2026-02-26 14:17:27 +07:00
amlannandy
e731c1c41b chore: additional changes 2026-02-26 14:03:14 +07:00
amlannandy
f407b3355b chore: address comments 2026-02-26 14:03:14 +07:00
amlannandy
4f5b95e791 chore: address comments 2026-02-26 14:03:14 +07:00
amlannandy
95d3928b32 chore: minor fix 2026-02-26 14:03:14 +07:00
amlannandy
8db22a5176 chore: minor fix 2026-02-26 14:03:14 +07:00
amlannandy
6ca329ba10 chore: metrics explorer v2 api migration in inspect page 2026-02-26 14:03:14 +07:00
75 changed files with 1065 additions and 938 deletions

View File

@@ -26,7 +26,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/utils/times"
"github.com/SigNoz/signoz/pkg/query-service/utils/timestamp"
"github.com/SigNoz/signoz/pkg/query-service/formatter"
"github.com/SigNoz/signoz/pkg/units"
baserules "github.com/SigNoz/signoz/pkg/query-service/rules"
@@ -335,7 +335,7 @@ func (r *AnomalyRule) Eval(ctx context.Context, ts time.Time) (int, error) {
prevState := r.State()
valueFormatter := formatter.FromUnit(r.Unit())
valueFormatter := units.FormatterFromUnit(r.Unit())
var res ruletypes.Vector
var err error

View File

@@ -85,6 +85,7 @@ interface QuerySearchProps {
signalSource?: string;
hardcodedAttributeKeys?: QueryKeyDataSuggestionsProps[];
onRun?: (query: string) => void;
showFilterSuggestionsWithoutMetric?: boolean;
}
function QuerySearch({
@@ -95,6 +96,7 @@ function QuerySearch({
onRun,
signalSource,
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
}: QuerySearchProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
@@ -251,7 +253,8 @@ function QuerySearch({
async (searchText?: string): Promise<void> => {
if (
dataSource === DataSource.METRICS &&
!queryData.aggregateAttribute?.key
!queryData.aggregateAttribute?.key &&
!showFilterSuggestionsWithoutMetric
) {
setKeySuggestions([]);
return;
@@ -300,6 +303,7 @@ function QuerySearch({
queryData.aggregateAttribute?.key,
signalSource,
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
],
);
@@ -1556,6 +1560,7 @@ QuerySearch.defaultProps = {
hardcodedAttributeKeys: undefined,
placeholder:
"Enter your filter query (e.g., http.status_code >= 500 AND service.name = 'frontend')",
showFilterSuggestionsWithoutMetric: false,
};
export default QuerySearch;

View File

@@ -6,6 +6,7 @@ import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
import { initialQueriesMap } from 'constants/queryBuilder';
import { fireEvent, render, userEvent, waitFor } from 'tests/test-utils';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import type { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
@@ -367,4 +368,36 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
dispatchSpy.mockRestore();
});
it('fetches key suggestions for metrics even without aggregateAttribute.key when showFilterSuggestionsWithoutMetric is true', async () => {
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
typeof getKeySuggestions
>;
mockedGetKeys.mockClear();
const queryData = {
...initialQueriesMap.metrics.builder.queryData[0],
aggregateAttribute: {
key: '',
dataType: DataTypes.String,
type: 'string',
},
};
render(
<QuerySearch
onChange={jest.fn()}
queryData={queryData}
dataSource={DataSource.METRICS}
showFilterSuggestionsWithoutMetric
/>,
);
await waitFor(
() => {
expect(mockedGetKeys).toHaveBeenCalled();
},
{ timeout: 2000 },
);
});
});

View File

@@ -453,9 +453,6 @@ function K8sClustersList({
const handleRowClick = (record: K8sClustersRowData): void => {
if (groupBy.length === 0) {
if (!record.clusterNameRaw) {
return;
}
setSelectedRowData(null);
setselectedClusterName(record.clusterUID);
setSearchParams({
@@ -520,13 +517,9 @@ function K8sClustersList({
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
if (record.clusterNameRaw) {
setselectedClusterName(record.clusterUID);
}
setselectedClusterName(record.clusterUID);
},
className: record.clusterNameRaw
? 'expanded-clickable-row'
: 'disabled-row',
className: 'expanded-clickable-row',
})}
/>
@@ -716,10 +709,7 @@ function K8sClustersList({
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className:
groupBy.length > 0 || record.clusterNameRaw
? 'clickable-row'
: 'disabled-row',
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,

View File

@@ -47,7 +47,6 @@ export const defaultAddedColumns: IEntityColumn[] = [
export interface K8sClustersRowData {
key: string;
clusterUID: string;
clusterNameRaw: string;
clusterName: React.ReactNode;
cpu: React.ReactNode;
memory: React.ReactNode;
@@ -176,7 +175,6 @@ export const formatDataForTable = (
data.map((cluster, index) => ({
key: index.toString(),
clusterUID: cluster.meta.k8s_cluster_name,
clusterNameRaw: cluster.meta.k8s_cluster_name || '',
clusterName: (
<Tooltip title={cluster.meta.k8s_cluster_name}>
{cluster.meta.k8s_cluster_name}

View File

@@ -459,9 +459,6 @@ function K8sDaemonSetsList({
const handleRowClick = (record: K8sDaemonSetsRowData): void => {
if (groupBy.length === 0) {
if (!record.daemonsetNameRaw) {
return;
}
setSelectedRowData(null);
setSelectedDaemonSetUID(record.daemonsetUID);
setSearchParams({
@@ -526,13 +523,9 @@ function K8sDaemonSetsList({
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
if (record.daemonsetNameRaw) {
setSelectedDaemonSetUID(record.daemonsetUID);
}
setSelectedDaemonSetUID(record.daemonsetUID);
},
className: record.daemonsetNameRaw
? 'expanded-clickable-row'
: 'disabled-row',
className: 'expanded-clickable-row',
})}
/>
@@ -724,10 +717,7 @@ function K8sDaemonSetsList({
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className:
groupBy.length > 0 || record.daemonsetNameRaw
? 'clickable-row'
: 'disabled-row',
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,

View File

@@ -82,7 +82,6 @@ export const defaultAddedColumns: IEntityColumn[] = [
export interface K8sDaemonSetsRowData {
key: string;
daemonsetUID: string;
daemonsetNameRaw: string;
daemonsetName: React.ReactNode;
cpu_request: React.ReactNode;
cpu_limit: React.ReactNode;
@@ -277,7 +276,6 @@ export const formatDataForTable = (
data.map((daemonSet, index) => ({
key: index.toString(),
daemonsetUID: daemonSet.daemonSetName,
daemonsetNameRaw: daemonSet.meta.k8s_daemonset_name || '',
daemonsetName: (
<Tooltip title={daemonSet.meta.k8s_daemonset_name}>
{daemonSet.meta.k8s_daemonset_name || ''}

View File

@@ -465,9 +465,6 @@ function K8sDeploymentsList({
const handleRowClick = (record: K8sDeploymentsRowData): void => {
if (groupBy.length === 0) {
if (!record.deploymentNameRaw) {
return;
}
setSelectedRowData(null);
setselectedDeploymentUID(record.deploymentUID);
setSearchParams({
@@ -532,13 +529,9 @@ function K8sDeploymentsList({
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
if (record.deploymentNameRaw) {
setselectedDeploymentUID(record.deploymentUID);
}
setselectedDeploymentUID(record.deploymentUID);
},
className: record.deploymentNameRaw
? 'expanded-clickable-row'
: 'disabled-row',
className: 'expanded-clickable-row',
})}
/>
@@ -731,10 +724,7 @@ function K8sDeploymentsList({
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className:
groupBy.length > 0 || record.deploymentNameRaw
? 'clickable-row'
: 'disabled-row',
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,

View File

@@ -81,7 +81,6 @@ export const defaultAddedColumns: IEntityColumn[] = [
export interface K8sDeploymentsRowData {
key: string;
deploymentUID: string;
deploymentNameRaw: string;
deploymentName: React.ReactNode;
available_pods: React.ReactNode;
desired_pods: React.ReactNode;
@@ -268,7 +267,6 @@ export const formatDataForTable = (
data.map((deployment, index) => ({
key: index.toString(),
deploymentUID: deployment.meta.k8s_deployment_name,
deploymentNameRaw: deployment.meta.k8s_deployment_name || '',
deploymentName: (
<Tooltip title={deployment.meta.k8s_deployment_name}>
{deployment.meta.k8s_deployment_name}

View File

@@ -337,11 +337,6 @@
cursor: pointer;
}
.disabled-row {
cursor: default;
opacity: 0.6;
}
.k8s-list-table {
.ant-table {
.ant-table-thead > tr > th {

View File

@@ -430,9 +430,6 @@ function K8sJobsList({
const handleRowClick = (record: K8sJobsRowData): void => {
if (groupBy.length === 0) {
if (!record.jobNameRaw) {
return;
}
setSelectedRowData(null);
setselectedJobUID(record.jobUID);
setSearchParams({
@@ -497,11 +494,9 @@ function K8sJobsList({
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
if (record.jobNameRaw) {
setselectedJobUID(record.jobUID);
}
setselectedJobUID(record.jobUID);
},
className: record.jobNameRaw ? 'expanded-clickable-row' : 'disabled-row',
className: 'expanded-clickable-row',
})}
/>
@@ -691,10 +686,7 @@ function K8sJobsList({
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className:
groupBy.length > 0 || record.jobNameRaw
? 'clickable-row'
: 'disabled-row',
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,

View File

@@ -94,7 +94,6 @@ export const defaultAddedColumns: IEntityColumn[] = [
export interface K8sJobsRowData {
key: string;
jobUID: string;
jobNameRaw: string;
jobName: React.ReactNode;
namespaceName: React.ReactNode;
successful_pods: React.ReactNode;
@@ -304,7 +303,6 @@ export const formatDataForTable = (
data.map((job, index) => ({
key: index.toString(),
jobUID: job.jobName,
jobNameRaw: job.meta.k8s_job_name || '',
jobName: (
<Tooltip title={job.meta.k8s_job_name}>
{job.meta.k8s_job_name || ''}

View File

@@ -461,9 +461,6 @@ function K8sNamespacesList({
const handleRowClick = (record: K8sNamespacesRowData): void => {
if (groupBy.length === 0) {
if (!record.namespaceNameRaw) {
return;
}
setSelectedRowData(null);
setselectedNamespaceUID(record.namespaceUID);
setSearchParams({
@@ -528,13 +525,9 @@ function K8sNamespacesList({
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
if (record.namespaceNameRaw) {
setselectedNamespaceUID(record.namespaceUID);
}
setselectedNamespaceUID(record.namespaceUID);
},
className: record.namespaceNameRaw
? 'expanded-clickable-row'
: 'disabled-row',
className: 'expanded-clickable-row',
})}
/>
@@ -725,10 +718,7 @@ function K8sNamespacesList({
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className:
groupBy.length > 0 || record.namespaceNameRaw
? 'clickable-row'
: 'disabled-row',
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,

View File

@@ -41,7 +41,6 @@ export const defaultAddedColumns: IEntityColumn[] = [
export interface K8sNamespacesRowData {
key: string;
namespaceUID: string;
namespaceNameRaw: string;
namespaceName: string;
clusterName: string;
cpu: React.ReactNode;
@@ -162,7 +161,6 @@ export const formatDataForTable = (
data.map((namespace, index) => ({
key: index.toString(),
namespaceUID: namespace.namespaceName,
namespaceNameRaw: namespace.namespaceName || '',
namespaceName: namespace.namespaceName,
clusterName: namespace.meta.k8s_cluster_name,
cpu: (

View File

@@ -440,9 +440,6 @@ function K8sNodesList({
const handleRowClick = (record: K8sNodesRowData): void => {
if (groupBy.length === 0) {
if (!record.nodeNameRaw) {
return;
}
setSelectedRowData(null);
setSelectedNodeUID(record.nodeUID);
setSearchParams({
@@ -508,13 +505,9 @@ function K8sNodesList({
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
if (record.nodeNameRaw) {
setSelectedNodeUID(record.nodeUID);
}
setSelectedNodeUID(record.nodeUID);
},
className: record.nodeNameRaw
? 'expanded-clickable-row'
: 'disabled-row',
className: 'expanded-clickable-row',
})}
/>
@@ -704,10 +697,7 @@ function K8sNodesList({
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className:
groupBy.length > 0 || record.nodeNameRaw
? 'clickable-row'
: 'disabled-row',
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,

View File

@@ -53,7 +53,6 @@ export const defaultAddedColumns: IEntityColumn[] = [
export interface K8sNodesRowData {
key: string;
nodeUID: string;
nodeNameRaw: string;
nodeName: React.ReactNode;
clusterName: string;
cpu: React.ReactNode;
@@ -194,7 +193,6 @@ export const formatDataForTable = (
data.map((node, index) => ({
key: `${node.nodeUID}-${index}`,
nodeUID: node.nodeUID || '',
nodeNameRaw: node.meta.k8s_node_name || '',
nodeName: (
<Tooltip title={node.meta.k8s_node_name}>
{node.meta.k8s_node_name || ''}

View File

@@ -497,9 +497,6 @@ function K8sPodsList({
const handleRowClick = (record: K8sPodsRowData): void => {
if (groupBy.length === 0) {
if (!record.podNameRaw) {
return;
}
setSelectedPodUID(record.podUID);
setSearchParams({
...Object.fromEntries(searchParams.entries()),
@@ -620,11 +617,9 @@ function K8sPodsList({
}}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
if (record.podNameRaw) {
setSelectedPodUID(record.podUID);
}
setSelectedPodUID(record.podUID);
},
className: record.podNameRaw ? 'expanded-clickable-row' : 'disabled-row',
className: 'expanded-clickable-row',
})}
/>
@@ -759,10 +754,7 @@ function K8sPodsList({
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className:
groupBy.length > 0 || record.podNameRaw
? 'clickable-row'
: 'disabled-row',
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,

View File

@@ -462,9 +462,6 @@ function K8sStatefulSetsList({
const handleRowClick = (record: K8sStatefulSetsRowData): void => {
if (groupBy.length === 0) {
if (!record.statefulsetNameRaw) {
return;
}
setSelectedRowData(null);
setselectedStatefulSetUID(record.statefulsetUID);
setSearchParams({
@@ -529,13 +526,9 @@ function K8sStatefulSetsList({
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
if (record.statefulsetNameRaw) {
setselectedStatefulSetUID(record.statefulsetUID);
}
setselectedStatefulSetUID(record.statefulsetUID);
},
className: record.statefulsetNameRaw
? 'expanded-clickable-row'
: 'disabled-row',
className: 'expanded-clickable-row',
})}
/>
@@ -727,10 +720,7 @@ function K8sStatefulSetsList({
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className:
groupBy.length > 0 || record.statefulsetNameRaw
? 'clickable-row'
: 'disabled-row',
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,

View File

@@ -82,7 +82,6 @@ export const defaultAddedColumns: IEntityColumn[] = [
export interface K8sStatefulSetsRowData {
key: string;
statefulsetUID: string;
statefulsetNameRaw: string;
statefulsetName: React.ReactNode;
cpu_request: React.ReactNode;
cpu_limit: React.ReactNode;
@@ -277,7 +276,6 @@ export const formatDataForTable = (
data.map((statefulSet, index) => ({
key: index.toString(),
statefulsetUID: statefulSet.statefulSetName,
statefulsetNameRaw: statefulSet.meta.k8s_statefulset_name || '',
statefulsetName: (
<Tooltip title={statefulSet.meta.k8s_statefulset_name}>
{statefulSet.meta.k8s_statefulset_name || ''}

View File

@@ -392,9 +392,6 @@ function K8sVolumesList({
const handleRowClick = (record: K8sVolumesRowData): void => {
if (groupBy.length === 0) {
if (!record.volumeNameRaw) {
return;
}
setSelectedRowData(null);
setselectedVolumeUID(record.volumeUID);
setSearchParams({
@@ -459,13 +456,9 @@ function K8sVolumesList({
showHeader={false}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => {
if (record.volumeNameRaw) {
setselectedVolumeUID(record.volumeUID);
}
setselectedVolumeUID(record.volumeUID);
},
className: record.volumeNameRaw
? 'expanded-clickable-row'
: 'disabled-row',
className: 'expanded-clickable-row',
})}
/>
@@ -650,10 +643,7 @@ function K8sVolumesList({
onChange={handleTableChange}
onRow={(record): { onClick: () => void; className: string } => ({
onClick: (): void => handleRowClick(record),
className:
groupBy.length > 0 || record.volumeNameRaw
? 'clickable-row'
: 'disabled-row',
className: 'clickable-row',
})}
expandable={{
expandedRowRender: isGroupedByAttribute ? expandedRowRender : undefined,

View File

@@ -47,7 +47,6 @@ export const defaultAddedColumns: IEntityColumn[] = [
export interface K8sVolumesRowData {
key: string;
volumeUID: string;
volumeNameRaw: string;
pvcName: React.ReactNode;
namespaceName: React.ReactNode;
capacity: React.ReactNode;
@@ -187,7 +186,6 @@ export const formatDataForTable = (
data.map((volume, index) => ({
key: index.toString(),
volumeUID: volume.persistentVolumeClaimName,
volumeNameRaw: volume.persistentVolumeClaimName || '',
pvcName: (
<Tooltip title={volume.persistentVolumeClaimName}>
{volume.persistentVolumeClaimName || ''}

View File

@@ -107,7 +107,6 @@ export const defaultAvailableColumns = [
export interface K8sPodsRowData {
key: string;
podName: React.ReactNode;
podNameRaw: string;
podUID: string;
cpu_request: React.ReactNode;
cpu_limit: React.ReactNode;
@@ -351,7 +350,6 @@ export const formatDataForTable = (
{pod.meta.k8s_pod_name || ''}
</Tooltip>
),
podNameRaw: pod.meta.k8s_pod_name || '',
podUID: pod.podUID || '',
cpu_request: (
<ValidateColumnValueWrapper

View File

@@ -33,7 +33,7 @@ function ExpandedView({
options,
spaceAggregationSeriesMap,
step,
metricInspectionOptions,
metricInspectionAppliedOptions,
timeAggregatedSeriesMap,
}: ExpandedViewProps): JSX.Element {
const [
@@ -44,17 +44,17 @@ function ExpandedView({
useEffect(() => {
logEvent(MetricsExplorerEvents.InspectPointClicked, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
[MetricsExplorerEventKeys.Filters]: metricInspectionOptions.filters,
[MetricsExplorerEventKeys.Filters]: metricInspectionAppliedOptions.filters,
[MetricsExplorerEventKeys.TimeAggregationInterval]:
metricInspectionOptions.timeAggregationInterval,
metricInspectionAppliedOptions.timeAggregationInterval,
[MetricsExplorerEventKeys.TimeAggregationOption]:
metricInspectionOptions.timeAggregationOption,
metricInspectionAppliedOptions.timeAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationOption]:
metricInspectionOptions.spaceAggregationOption,
metricInspectionAppliedOptions.spaceAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationLabels]:
metricInspectionOptions.spaceAggregationLabels,
metricInspectionAppliedOptions.spaceAggregationLabels,
});
}, [metricInspectionOptions]);
}, [metricInspectionAppliedOptions]);
useEffect(() => {
if (step !== InspectionStep.COMPLETED) {
@@ -167,7 +167,7 @@ function ExpandedView({
<Typography.Text strong>
{`${absoluteValue} is the ${
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW[
metricInspectionOptions.spaceAggregationOption ??
metricInspectionAppliedOptions.spaceAggregationOption ??
SpaceAggregationOptions.SUM_BY
]
} of`}
@@ -240,7 +240,7 @@ function ExpandedView({
)?.value ?? options?.value
} is the ${
TIME_AGGREGATION_OPTIONS[
metricInspectionOptions.timeAggregationOption ??
metricInspectionAppliedOptions.timeAggregationOption ??
TimeAggregationOptions.SUM
]
} of`
@@ -299,7 +299,7 @@ function ExpandedView({
<Typography.Text strong>
{`${absoluteValue} is the ${
TIME_AGGREGATION_OPTIONS[
metricInspectionOptions.timeAggregationOption ??
metricInspectionAppliedOptions.timeAggregationOption ??
TimeAggregationOptions.SUM
]
} of`}

View File

@@ -13,9 +13,10 @@ import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import { formatNumberIntoHumanReadableFormat } from '../Summary/utils';
import { METRIC_TYPE_TO_COLOR_MAP, METRIC_TYPE_TO_ICON_MAP } from './constants';
import GraphPopover from './GraphPopover';
import HoverPopover from './HoverPopover';
import TableView from './TableView';
import { GraphPopoverOptions, GraphViewProps } from './types';
import { HoverPopover, onGraphClick, onGraphHover } from './utils';
import { onGraphClick, onGraphHover } from './utils';
function GraphView({
inspectMetricsTimeSeries,
@@ -29,7 +30,7 @@ function GraphView({
popoverOptions,
setShowExpandedView,
setExpandedViewOptions,
metricInspectionOptions,
metricInspectionAppliedOptions,
isInspectMetricsRefetching,
}: GraphViewProps): JSX.Element {
const isDarkMode = useIsDarkMode();
@@ -233,7 +234,7 @@ function GraphView({
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
setShowExpandedView={setShowExpandedView}
setExpandedViewOptions={setExpandedViewOptions}
metricInspectionOptions={metricInspectionOptions}
metricInspectionAppliedOptions={metricInspectionAppliedOptions}
isInspectMetricsRefetching={isInspectMetricsRefetching}
/>
)}
@@ -255,7 +256,7 @@ function GraphView({
<HoverPopover
options={hoverPopoverOptions}
step={inspectionStep}
metricInspectionOptions={metricInspectionOptions}
metricInspectionAppliedOptions={metricInspectionAppliedOptions}
/>
)}
</div>

View File

@@ -0,0 +1,111 @@
import { useMemo } from 'react';
import { Card, Typography } from 'antd';
import {
GraphPopoverOptions,
InspectionStep,
MetricInspectionOptions,
} from './types';
import { TimeSeriesLabelProps } from './types';
import { formatTimestampToFullDateTime } from './utils';
function TimeSeriesLabel({
timeSeries,
textColor,
}: TimeSeriesLabelProps): JSX.Element {
return (
<>
{Object.entries(timeSeries?.labels ?? {}).map(([key, value]) => (
<span key={key}>
<Typography.Text style={{ color: textColor, fontWeight: 600 }}>
{key}
</Typography.Text>
: {value}{' '}
</span>
))}
</>
);
}
function HoverPopover({
options,
step,
metricInspectionAppliedOptions,
}: {
options: GraphPopoverOptions;
step: InspectionStep;
metricInspectionAppliedOptions: MetricInspectionOptions;
}): JSX.Element {
const closestTimestamp = useMemo(() => {
if (!options.timeSeries) {
return options.timestamp;
}
return options.timeSeries?.values.reduce((prev, curr) => {
const prevDiff = Math.abs(prev.timestamp - options.timestamp);
const currDiff = Math.abs(curr.timestamp - options.timestamp);
return prevDiff < currDiff ? prev : curr;
}).timestamp;
}, [options.timeSeries, options.timestamp]);
const closestValue = useMemo(() => {
if (!options.timeSeries) {
return options.value;
}
const index = options.timeSeries.values.findIndex(
(value) => value.timestamp === closestTimestamp,
);
return index !== undefined && index >= 0
? options.timeSeries?.values[index].value
: null;
}, [options.timeSeries, closestTimestamp, options.value]);
const title = useMemo(() => {
if (
step === InspectionStep.COMPLETED &&
metricInspectionAppliedOptions.spaceAggregationLabels.length === 0
) {
return undefined;
}
if (step === InspectionStep.COMPLETED && options.timeSeries?.title) {
return options.timeSeries.title;
}
if (!options.timeSeries) {
return undefined;
}
return (
<TimeSeriesLabel
timeSeries={options.timeSeries}
textColor={options.timeSeries?.strokeColor}
/>
);
}, [step, options.timeSeries, metricInspectionAppliedOptions]);
return (
<Card
className="hover-popover-card"
style={{
top: options.y + 10,
left: options.x + 10,
}}
>
<div className="hover-popover-row">
<Typography.Text>
{formatTimestampToFullDateTime(closestTimestamp ?? 0)}
</Typography.Text>
<Typography.Text>{Number(closestValue).toFixed(2)}</Typography.Text>
</div>
{options.timeSeries && (
<Typography.Text
style={{
color: options.timeSeries?.strokeColor,
fontWeight: 200,
}}
>
{title}
</Typography.Text>
)}
</Card>
);
}
export default HoverPopover;

View File

@@ -122,6 +122,10 @@
gap: 4px;
.inspect-metrics-query-builder-header {
display: flex;
align-items: center;
justify-content: space-between;
.query-builder-button-label {
display: flex;
align-items: center;
@@ -257,6 +261,21 @@
.completed-checklist-container {
margin-left: 20px;
.completed-checklist-item,
.whats-next-checklist-item {
.completed-checklist-item-title,
.whats-next-checklist-item-title {
display: flex;
align-items: center;
gap: 4px;
.ant-typography {
display: flex;
align-items: center;
gap: 4px;
}
}
}
}
.completed-message-container {

View File

@@ -9,6 +9,7 @@ import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Compass } from 'lucide-react';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
@@ -31,7 +32,12 @@ function Inspect({
onClose,
}: InspectProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [metricName, setMetricName] = useState<string | null>(defaultMetricName);
const [currentMetricName, setCurrentMetricName] = useState<string>(
defaultMetricName,
);
const [appliedMetricName, setAppliedMetricName] = useState<string>(
defaultMetricName,
);
const [
popoverOptions,
setPopoverOptions,
@@ -42,9 +48,12 @@ function Inspect({
] = useState<GraphPopoverOptions | null>(null);
const [showExpandedView, setShowExpandedView] = useState(false);
const { data: metricDetailsData } = useGetMetricDetails(metricName ?? '', {
enabled: !!metricName,
});
const { data: metricDetailsData } = useGetMetricDetails(
appliedMetricName ?? '',
{
enabled: !!appliedMetricName,
},
);
const { currentQuery } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
@@ -97,25 +106,16 @@ function Inspect({
aggregatedTimeSeries,
timeAggregatedSeriesMap,
reset,
} = useInspectMetrics(metricName);
} = useInspectMetrics(appliedMetricName);
const handleDispatchMetricInspectionOptions = useCallback(
(action: MetricInspectionAction): void => {
dispatchMetricInspectionOptions(action);
logEvent(MetricsExplorerEvents.InspectQueryChanged, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
[MetricsExplorerEventKeys.Filters]: metricInspectionOptions.filters,
[MetricsExplorerEventKeys.TimeAggregationInterval]:
metricInspectionOptions.timeAggregationInterval,
[MetricsExplorerEventKeys.TimeAggregationOption]:
metricInspectionOptions.timeAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationOption]:
metricInspectionOptions.spaceAggregationOption,
[MetricsExplorerEventKeys.SpaceAggregationLabels]:
metricInspectionOptions.spaceAggregationLabels,
});
},
[dispatchMetricInspectionOptions, metricInspectionOptions],
[dispatchMetricInspectionOptions],
);
const selectedMetricType = useMemo(
@@ -128,18 +128,39 @@ function Inspect({
[metricDetailsData],
);
const aggregateAttribute = useMemo(
() => ({
key: currentMetricName ?? '',
dataType: DataTypes.String,
type: selectedMetricType as string,
isColumn: true,
isJSON: false,
id: `${currentMetricName}--${DataTypes.String}--${selectedMetricType}--true`,
}),
[currentMetricName, selectedMetricType],
);
const [currentQueryData, setCurrentQueryData] = useState<IBuilderQuery>({
...searchQuery,
aggregateAttribute,
});
useEffect(() => {
if (searchQuery) {
setCurrentQueryData({
...searchQuery,
aggregateAttribute,
});
}
}, [aggregateAttribute, searchQuery]);
const resetInspection = useCallback(() => {
setShowExpandedView(false);
setPopoverOptions(null);
setExpandedViewOptions(null);
setCurrentQueryData(searchQuery as IBuilderQuery);
reset();
}, [reset]);
// Reset inspection when the selected metric changes
useEffect(() => {
resetInspection();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [metricName]);
}, [reset, searchQuery]);
// Hide expanded view whenever inspection step changes
useEffect(() => {
@@ -193,7 +214,7 @@ function Inspect({
inspectMetricsTimeSeries={aggregatedTimeSeries}
formattedInspectMetricsTimeSeries={formattedInspectMetricsTimeSeries}
resetInspection={resetInspection}
metricName={metricName}
metricName={appliedMetricName}
metricUnit={selectedMetricUnit}
metricType={selectedMetricType}
spaceAggregationSeriesMap={spaceAggregationSeriesMap}
@@ -203,19 +224,20 @@ function Inspect({
showExpandedView={showExpandedView}
setExpandedViewOptions={setExpandedViewOptions}
popoverOptions={popoverOptions}
metricInspectionOptions={metricInspectionOptions}
metricInspectionAppliedOptions={metricInspectionOptions.appliedOptions}
isInspectMetricsRefetching={isInspectMetricsRefetching}
/>
<QueryBuilder
metricName={metricName}
metricType={selectedMetricType}
setMetricName={setMetricName}
currentMetricName={currentMetricName}
setCurrentMetricName={setCurrentMetricName}
setAppliedMetricName={setAppliedMetricName}
spaceAggregationLabels={spaceAggregationLabels}
metricInspectionOptions={metricInspectionOptions}
currentMetricInspectionOptions={metricInspectionOptions.currentOptions}
dispatchMetricInspectionOptions={handleDispatchMetricInspectionOptions}
inspectionStep={inspectionStep}
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
searchQuery={searchQuery as IBuilderQuery}
currentQuery={currentQueryData}
setCurrentQuery={setCurrentQueryData}
/>
</div>
<div className="inspect-metrics-content-second-col">
@@ -228,7 +250,7 @@ function Inspect({
options={expandedViewOptions}
spaceAggregationSeriesMap={spaceAggregationSeriesMap}
step={inspectionStep}
metricInspectionOptions={metricInspectionOptions}
metricInspectionAppliedOptions={metricInspectionOptions.appliedOptions}
timeAggregatedSeriesMap={timeAggregatedSeriesMap}
/>
)}
@@ -244,17 +266,21 @@ function Inspect({
aggregatedTimeSeries,
formattedInspectMetricsTimeSeries,
resetInspection,
metricName,
appliedMetricName,
selectedMetricUnit,
selectedMetricType,
spaceAggregationSeriesMap,
inspectionStep,
showExpandedView,
popoverOptions,
metricInspectionOptions,
metricInspectionOptions.appliedOptions,
metricInspectionOptions.currentOptions,
currentMetricName,
setCurrentMetricName,
setAppliedMetricName,
spaceAggregationLabels,
handleDispatchMetricInspectionOptions,
searchQuery,
currentQueryData,
expandedViewOptions,
timeAggregatedSeriesMap,
]);

View File

@@ -0,0 +1,57 @@
import { useCallback } from 'react';
import { Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import { convertExpressionToFilters } from 'components/QueryBuilderV2/utils';
import { DataSource } from 'types/common/queryBuilder';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import { MetricFiltersProps } from './types';
function MetricFilters({
dispatchMetricInspectionOptions,
currentQuery,
setCurrentQuery,
}: MetricFiltersProps): JSX.Element {
const handleOnChange = useCallback(
(expression: string): void => {
logEvent(MetricsExplorerEvents.FilterApplied, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
});
const tagFilter = {
items: convertExpressionToFilters(expression),
op: 'AND',
};
setCurrentQuery({
...currentQuery,
filters: tagFilter,
filter: {
...currentQuery.filter,
expression,
},
expression,
});
dispatchMetricInspectionOptions({
type: 'SET_FILTERS',
payload: tagFilter,
});
},
[currentQuery, dispatchMetricInspectionOptions, setCurrentQuery],
);
return (
<div
data-testid="metric-filters"
className="inspect-metrics-input-group metric-filters"
>
<Typography.Text>Where</Typography.Text>
<QuerySearch
queryData={currentQuery}
onChange={handleOnChange}
dataSource={DataSource.METRICS}
/>
</div>
);
}
export default MetricFilters;

View File

@@ -0,0 +1,40 @@
import { useState } from 'react';
import { Typography } from 'antd';
import { initialQueriesMap } from 'constants/queryBuilder';
import { AggregatorFilter } from 'container/QueryBuilder/filters';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { MetricNameSearchProps } from './types';
function MetricNameSearch({
currentMetricName,
setCurrentMetricName,
}: MetricNameSearchProps): JSX.Element {
const [searchText, setSearchText] = useState(currentMetricName);
const handleSetMetricName = (value: BaseAutocompleteData): void => {
setCurrentMetricName(value.key);
};
const handleChange = (value: BaseAutocompleteData): void => {
setSearchText(value.key);
};
return (
<div
data-testid="metric-name-search"
className="inspect-metrics-input-group metric-name-search"
>
<Typography.Text>From</Typography.Text>
<AggregatorFilter
defaultValue={searchText ?? ''}
query={initialQueriesMap[DataSource.METRICS].builder.queryData[0]}
onSelect={handleSetMetricName}
onChange={handleChange}
/>
</div>
);
}
export default MetricNameSearch;

View File

@@ -0,0 +1,73 @@
import { Typography } from 'antd';
import { Select } from 'antd';
import classNames from 'classnames';
import { SPACE_AGGREGATION_OPTIONS } from './constants';
import { InspectionStep } from './types';
import { MetricSpaceAggregationProps } from './types';
function MetricSpaceAggregation({
spaceAggregationLabels,
currentMetricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep,
}: MetricSpaceAggregationProps): JSX.Element {
return (
<div
data-testid="metric-space-aggregation"
className="metric-space-aggregation"
>
<div
className={classNames('metric-space-aggregation-header', {
'selected-step': inspectionStep === InspectionStep.SPACE_AGGREGATION,
})}
>
<Typography.Text>AGGREGATE BY LABELS</Typography.Text>
</div>
<div className="metric-space-aggregation-content">
<div className="metric-space-aggregation-content-left">
<Select
value={currentMetricInspectionOptions.spaceAggregationOption}
placeholder="Select option"
onChange={(value): void => {
dispatchMetricInspectionOptions({
type: 'SET_SPACE_AGGREGATION_OPTION',
payload: value,
});
}}
style={{ width: 130 }}
disabled={inspectionStep === InspectionStep.TIME_AGGREGATION}
>
{/* eslint-disable-next-line sonarjs/no-identical-functions */}
{Object.entries(SPACE_AGGREGATION_OPTIONS).map(([key, value]) => (
<Select.Option key={key} value={key}>
{value}
</Select.Option>
))}
</Select>
</div>
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="Search for attributes..."
value={currentMetricInspectionOptions.spaceAggregationLabels}
onChange={(value): void => {
dispatchMetricInspectionOptions({
type: 'SET_SPACE_AGGREGATION_LABELS',
payload: value,
});
}}
disabled={inspectionStep === InspectionStep.TIME_AGGREGATION}
>
{spaceAggregationLabels.map((label) => (
<Select.Option key={label} value={label}>
{label}
</Select.Option>
))}
</Select>
</div>
</div>
);
}
export default MetricSpaceAggregation;

View File

@@ -0,0 +1,80 @@
import { Input, Typography } from 'antd';
import { Select } from 'antd';
import classNames from 'classnames';
import { TIME_AGGREGATION_OPTIONS } from './constants';
import { InspectionStep } from './types';
import { MetricTimeAggregationProps } from './types';
import { getDefaultTimeAggregationInterval } from './utils';
function MetricTimeAggregation({
currentMetricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep,
inspectMetricsTimeSeries,
}: MetricTimeAggregationProps): JSX.Element {
return (
<div
data-testid="metric-time-aggregation"
className="metric-time-aggregation"
>
<div
className={classNames('metric-time-aggregation-header', {
'selected-step': inspectionStep === InspectionStep.TIME_AGGREGATION,
})}
>
<Typography.Text>AGGREGATE BY TIME</Typography.Text>
</div>
<div className="metric-time-aggregation-content">
<div className="inspect-metrics-input-group">
<Typography.Text>Align with</Typography.Text>
<Select
value={currentMetricInspectionOptions.timeAggregationOption}
onChange={(value): void => {
dispatchMetricInspectionOptions({
type: 'SET_TIME_AGGREGATION_OPTION',
payload: value,
});
// set the time aggregation interval to the default value if it is not set
if (!currentMetricInspectionOptions.timeAggregationInterval) {
dispatchMetricInspectionOptions({
type: 'SET_TIME_AGGREGATION_INTERVAL',
payload: getDefaultTimeAggregationInterval(
inspectMetricsTimeSeries[0],
),
});
}
}}
style={{ width: 130 }}
placeholder="Select option"
>
{Object.entries(TIME_AGGREGATION_OPTIONS).map(([key, value]) => (
<Select.Option key={key} value={key}>
{value}
</Select.Option>
))}
</Select>
</div>
<div className="inspect-metrics-input-group">
<Typography.Text>aggregated every</Typography.Text>
<Input
type="number"
className="no-arrows-input"
value={currentMetricInspectionOptions.timeAggregationInterval}
placeholder="Select interval..."
suffix="seconds"
onChange={(e): void => {
dispatchMetricInspectionOptions({
type: 'SET_TIME_AGGREGATION_INTERVAL',
payload: parseInt(e.target.value, 10),
});
}}
onWheel={(e): void => (e.target as HTMLInputElement).blur()}
/>
</div>
</div>
</div>
);
}
export default MetricTimeAggregation;

View File

@@ -1,25 +1,33 @@
import { useCallback } from 'react';
import { Button, Card } from 'antd';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import { Atom } from 'lucide-react';
import MetricFilters from './MetricFilters';
import MetricNameSearch from './MetricNameSearch';
import MetricSpaceAggregation from './MetricSpaceAggregation';
import MetricTimeAggregation from './MetricTimeAggregation';
import { QueryBuilderProps } from './types';
import {
MetricFilters,
MetricNameSearch,
MetricSpaceAggregation,
MetricTimeAggregation,
} from './utils';
function QueryBuilder({
metricName,
setMetricName,
currentMetricName,
setCurrentMetricName,
setAppliedMetricName,
spaceAggregationLabels,
metricInspectionOptions,
currentMetricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep,
inspectMetricsTimeSeries,
searchQuery,
metricType,
currentQuery,
setCurrentQuery,
}: QueryBuilderProps): JSX.Element {
const applyInspectionOptions = useCallback(() => {
setAppliedMetricName(currentMetricName ?? '');
dispatchMetricInspectionOptions({
type: 'APPLY_METRIC_INSPECTION_OPTIONS',
});
}, [currentMetricName, setAppliedMetricName, dispatchMetricInspectionOptions]);
return (
<div className="inspect-metrics-query-builder">
<div className="inspect-metrics-query-builder-header">
@@ -31,25 +39,28 @@ function QueryBuilder({
>
Query Builder
</Button>
<RunQueryBtn onStageRunQuery={applyInspectionOptions} />
</div>
<Card className="inspect-metrics-query-builder-content">
<MetricNameSearch metricName={metricName} setMetricName={setMetricName} />
<MetricNameSearch
currentMetricName={currentMetricName}
setCurrentMetricName={setCurrentMetricName}
/>
<MetricFilters
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
searchQuery={searchQuery}
metricName={metricName}
metricType={metricType || null}
currentQuery={currentQuery}
setCurrentQuery={setCurrentQuery}
/>
<MetricTimeAggregation
inspectionStep={inspectionStep}
metricInspectionOptions={metricInspectionOptions}
currentMetricInspectionOptions={currentMetricInspectionOptions}
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
inspectMetricsTimeSeries={inspectMetricsTimeSeries}
/>
<MetricSpaceAggregation
inspectionStep={inspectionStep}
spaceAggregationLabels={spaceAggregationLabels}
metricInspectionOptions={metricInspectionOptions}
currentMetricInspectionOptions={currentMetricInspectionOptions}
dispatchMetricInspectionOptions={dispatchMetricInspectionOptions}
/>
</Card>

View File

@@ -11,13 +11,13 @@ function TableView({
setShowExpandedView,
setExpandedViewOptions,
isInspectMetricsRefetching,
metricInspectionOptions,
metricInspectionAppliedOptions,
}: TableViewProps): JSX.Element {
const isSpaceAggregatedWithoutLabel = useMemo(
() =>
!!metricInspectionOptions.spaceAggregationOption &&
metricInspectionOptions.spaceAggregationLabels.length === 0,
[metricInspectionOptions],
!!metricInspectionAppliedOptions.spaceAggregationOption &&
metricInspectionAppliedOptions.spaceAggregationLabels.length === 0,
[metricInspectionAppliedOptions],
);
const labelKeys = useMemo(() => {
if (isSpaceAggregatedWithoutLabel) {

View File

@@ -0,0 +1,23 @@
import { Typography } from 'antd';
import { TimeSeriesLabelProps } from './types';
function TimeSeriesLabel({
timeSeries,
textColor,
}: TimeSeriesLabelProps): JSX.Element {
return (
<>
{Object.entries(timeSeries?.labels ?? {}).map(([key, value]) => (
<span key={key}>
<Typography.Text style={{ color: textColor, fontWeight: 600 }}>
{key}
</Typography.Text>
: {value}{' '}
</span>
))}
</>
);
}
export default TimeSeriesLabel;

View File

@@ -62,7 +62,7 @@ describe('ExpandedView', () => {
],
]);
const mockMetricInspectionOptions: MetricInspectionOptions = {
const mockMetricInspectionAppliedOptions: MetricInspectionOptions = {
timeAggregationOption: TimeAggregationOptions.MAX,
timeAggregationInterval: 60,
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
@@ -79,7 +79,7 @@ describe('ExpandedView', () => {
options={mockOptions}
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
step={InspectionStep.TIME_AGGREGATION}
metricInspectionOptions={mockMetricInspectionOptions}
metricInspectionAppliedOptions={mockMetricInspectionAppliedOptions}
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
/>,
);
@@ -96,8 +96,8 @@ describe('ExpandedView', () => {
options={mockOptions}
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
step={InspectionStep.SPACE_AGGREGATION}
metricInspectionOptions={{
...mockMetricInspectionOptions,
metricInspectionAppliedOptions={{
...mockMetricInspectionAppliedOptions,
timeAggregationInterval: TIME_AGGREGATION_INTERVAL,
}}
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
@@ -112,7 +112,7 @@ describe('ExpandedView', () => {
screen.getByText(
`42.123 is the ${
TIME_AGGREGATION_OPTIONS[
mockMetricInspectionOptions.timeAggregationOption as TimeAggregationOptions
mockMetricInspectionAppliedOptions.timeAggregationOption as TimeAggregationOptions
]
} of`,
),
@@ -127,7 +127,7 @@ describe('ExpandedView', () => {
options={mockOptions}
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
step={InspectionStep.COMPLETED}
metricInspectionOptions={mockMetricInspectionOptions}
metricInspectionAppliedOptions={mockMetricInspectionAppliedOptions}
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
/>,
);
@@ -139,7 +139,7 @@ describe('ExpandedView', () => {
screen.getByText(
`42.123 is the ${
SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW[
mockMetricInspectionOptions.spaceAggregationOption as SpaceAggregationOptions
mockMetricInspectionAppliedOptions.spaceAggregationOption as SpaceAggregationOptions
]
} of`,
),
@@ -153,7 +153,7 @@ describe('ExpandedView', () => {
options={mockOptions}
spaceAggregationSeriesMap={mockSpaceAggregationSeriesMap}
step={InspectionStep.TIME_AGGREGATION}
metricInspectionOptions={mockMetricInspectionOptions}
metricInspectionAppliedOptions={mockMetricInspectionAppliedOptions}
timeAggregatedSeriesMap={mockTimeAggregatedSeriesMap}
/>,
);

View File

@@ -54,7 +54,7 @@ describe('GraphView', () => {
setExpandedViewOptions: jest.fn(),
resetInspection: jest.fn(),
showExpandedView: false,
metricInspectionOptions: {
metricInspectionAppliedOptions: {
timeAggregationInterval: 60,
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,
spaceAggregationLabels: ['host_name'],

View File

@@ -2,6 +2,7 @@
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MetricType } from 'api/metricsExplorer/getMetricsList';
import * as appContextHooks from 'providers/App/App';
import store from 'store';
@@ -22,6 +23,27 @@ jest.mock('react-router-dom', () => ({
}),
}));
jest.mock('container/QueryBuilder/filters', () => ({
AggregatorFilter: ({ onSelect, onChange, defaultValue }: any): JSX.Element => (
<div data-testid="mock-aggregator-filter">
<input
data-testid="metric-name-input"
defaultValue={defaultValue}
onChange={(e: React.ChangeEvent<HTMLInputElement>): void =>
onChange({ key: e.target.value })
}
/>
<button
type="button"
data-testid="select-metric-button"
onClick={(): void => onSelect({ key: 'test_metric_2' })}
>
Select Metric
</button>
</div>
),
}));
jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
user: {
role: 'admin',
@@ -48,12 +70,16 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
const queryClient = new QueryClient();
const mockSetCurrentMetricName = jest.fn();
const mockSetAppliedMetricName = jest.fn();
describe('QueryBuilder', () => {
const defaultProps = {
metricName: 'test_metric',
setMetricName: jest.fn(),
currentMetricName: 'test_metric',
setCurrentMetricName: mockSetCurrentMetricName,
setAppliedMetricName: mockSetAppliedMetricName,
spaceAggregationLabels: ['label1', 'label2'],
metricInspectionOptions: {
currentMetricInspectionOptions: {
timeAggregationInterval: 60,
timeAggregationOption: TimeAggregationOptions.AVG,
spaceAggregationLabels: [],
@@ -67,19 +93,20 @@ describe('QueryBuilder', () => {
metricType: MetricType.SUM,
inspectionStep: InspectionStep.TIME_AGGREGATION,
inspectMetricsTimeSeries: [],
searchQuery: {
currentQuery: {
filters: {
items: [],
op: 'and',
},
} as any,
setCurrentQuery: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
});
it('renders query builder header', () => {
it('renders query builder with all components', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
@@ -88,49 +115,53 @@ describe('QueryBuilder', () => {
</QueryClientProvider>,
);
expect(screen.getByText('Query Builder')).toBeInTheDocument();
});
it('renders metric name search component', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<QueryBuilder {...defaultProps} />
</Provider>
</QueryClientProvider>,
);
expect(screen.getByTestId('metric-name-search')).toBeInTheDocument();
});
it('renders metric filters component', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<QueryBuilder {...defaultProps} />
</Provider>
</QueryClientProvider>,
);
expect(screen.getByTestId('metric-filters')).toBeInTheDocument();
});
it('renders time aggregation component', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<QueryBuilder {...defaultProps} />
</Provider>
</QueryClientProvider>,
);
expect(screen.getByTestId('metric-time-aggregation')).toBeInTheDocument();
});
it('renders space aggregation component', () => {
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<QueryBuilder {...defaultProps} />
</Provider>
</QueryClientProvider>,
);
expect(screen.getByTestId('metric-space-aggregation')).toBeInTheDocument();
});
it('should call setCurrentMetricName when metric name is selected', async () => {
const user = userEvent.setup();
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<QueryBuilder {...defaultProps} />
</Provider>
</QueryClientProvider>,
);
const metricNameSearch = screen.getByTestId('metric-name-search');
expect(metricNameSearch).toBeInTheDocument();
expect(screen.getByText('From')).toBeInTheDocument();
const selectButton = screen.getByTestId('select-metric-button');
await user.click(selectButton);
expect(mockSetCurrentMetricName).toHaveBeenCalledWith('test_metric_2');
});
it('should call setAppliedMetricName and apply inspection options when query is applied', async () => {
const user = userEvent.setup();
render(
<QueryClientProvider client={queryClient}>
<Provider store={store}>
<QueryBuilder {...defaultProps} />
</Provider>
</QueryClientProvider>,
);
const applyQueryButton = screen.getByText('Run Query');
await user.click(applyQueryButton);
expect(mockSetCurrentMetricName).toHaveBeenCalledTimes(0);
expect(mockSetAppliedMetricName).toHaveBeenCalledWith('test_metric');
expect(defaultProps.dispatchMetricInspectionOptions).toHaveBeenCalledWith({
type: 'APPLY_METRIC_INSPECTION_OPTIONS',
});
});
});

View File

@@ -49,7 +49,7 @@ describe('TableView', () => {
inspectMetricsTimeSeries: mockTimeSeries,
setShowExpandedView: jest.fn(),
setExpandedViewOptions: jest.fn(),
metricInspectionOptions: {
metricInspectionAppliedOptions: {
timeAggregationInterval: 60,
timeAggregationOption: TimeAggregationOptions.MAX,
spaceAggregationOption: SpaceAggregationOptions.MAX_BY,

View File

@@ -11,7 +11,7 @@ import {
} from 'lucide-react';
import {
MetricInspectionOptions,
MetricInspectionState,
SpaceAggregationOptions,
TimeAggregationOptions,
} from './types';
@@ -71,14 +71,26 @@ export const SPACE_AGGREGATION_OPTIONS_FOR_EXPANDED_VIEW: Record<
[SpaceAggregationOptions.AVG_BY]: 'Avg',
};
export const INITIAL_INSPECT_METRICS_OPTIONS: MetricInspectionOptions = {
timeAggregationOption: undefined,
timeAggregationInterval: undefined,
spaceAggregationOption: undefined,
spaceAggregationLabels: [],
filters: {
items: [],
op: 'AND',
export const INITIAL_INSPECT_METRICS_OPTIONS: MetricInspectionState = {
currentOptions: {
timeAggregationOption: undefined,
timeAggregationInterval: undefined,
spaceAggregationOption: undefined,
spaceAggregationLabels: [],
filters: {
items: [],
op: 'AND',
},
},
appliedOptions: {
timeAggregationOption: undefined,
timeAggregationInterval: undefined,
spaceAggregationOption: undefined,
spaceAggregationLabels: [],
filters: {
items: [],
op: 'AND',
},
},
};

View File

@@ -7,7 +7,7 @@ import {
import { AlignedData } from 'uplot';
export type InspectProps = {
metricName: string | null;
metricName: string;
isOpen: boolean;
onClose: () => void;
};
@@ -19,7 +19,7 @@ export interface UseInspectMetricsReturnData {
isInspectMetricsError: boolean;
formattedInspectMetricsTimeSeries: AlignedData;
spaceAggregationLabels: string[];
metricInspectionOptions: MetricInspectionOptions;
metricInspectionOptions: MetricInspectionState;
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
inspectionStep: InspectionStep;
isInspectMetricsRefetching: boolean;
@@ -43,36 +43,36 @@ export interface GraphViewProps {
showExpandedView: boolean;
setShowExpandedView: (showExpandedView: boolean) => void;
setExpandedViewOptions: (options: GraphPopoverOptions | null) => void;
metricInspectionOptions: MetricInspectionOptions;
metricInspectionAppliedOptions: MetricInspectionOptions;
isInspectMetricsRefetching: boolean;
}
export interface QueryBuilderProps {
metricName: string | null;
setMetricName: (metricName: string) => void;
metricType: MetricType | undefined;
currentMetricName: string | null;
setCurrentMetricName: (metricName: string) => void;
setAppliedMetricName: (metricName: string) => void;
spaceAggregationLabels: string[];
metricInspectionOptions: MetricInspectionOptions;
currentMetricInspectionOptions: MetricInspectionOptions;
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
inspectionStep: InspectionStep;
inspectMetricsTimeSeries: InspectMetricsSeries[];
searchQuery: IBuilderQuery;
currentQuery: IBuilderQuery;
setCurrentQuery: (query: IBuilderQuery) => void;
}
export interface MetricNameSearchProps {
metricName: string | null;
setMetricName: (metricName: string) => void;
currentMetricName: string | null;
setCurrentMetricName: (metricName: string) => void;
}
export interface MetricFiltersProps {
searchQuery: IBuilderQuery;
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
metricName: string | null;
metricType: MetricType | null;
currentQuery: IBuilderQuery;
setCurrentQuery: (query: IBuilderQuery) => void;
}
export interface MetricTimeAggregationProps {
metricInspectionOptions: MetricInspectionOptions;
currentMetricInspectionOptions: MetricInspectionOptions;
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
inspectionStep: InspectionStep;
inspectMetricsTimeSeries: InspectMetricsSeries[];
@@ -80,7 +80,7 @@ export interface MetricTimeAggregationProps {
export interface MetricSpaceAggregationProps {
spaceAggregationLabels: string[];
metricInspectionOptions: MetricInspectionOptions;
currentMetricInspectionOptions: MetricInspectionOptions;
dispatchMetricInspectionOptions: (action: MetricInspectionAction) => void;
inspectionStep: InspectionStep;
}
@@ -109,13 +109,19 @@ export interface MetricInspectionOptions {
filters: TagFilter;
}
export interface MetricInspectionState {
currentOptions: MetricInspectionOptions;
appliedOptions: MetricInspectionOptions;
}
export type MetricInspectionAction =
| { type: 'SET_TIME_AGGREGATION_OPTION'; payload: TimeAggregationOptions }
| { type: 'SET_TIME_AGGREGATION_INTERVAL'; payload: number }
| { type: 'SET_SPACE_AGGREGATION_OPTION'; payload: SpaceAggregationOptions }
| { type: 'SET_SPACE_AGGREGATION_LABELS'; payload: string[] }
| { type: 'SET_FILTERS'; payload: TagFilter }
| { type: 'RESET_INSPECTION' };
| { type: 'RESET_INSPECTION' }
| { type: 'APPLY_METRIC_INSPECTION_OPTIONS' };
export enum InspectionStep {
TIME_AGGREGATION = 1,
@@ -156,7 +162,7 @@ export interface ExpandedViewProps {
options: GraphPopoverOptions | null;
spaceAggregationSeriesMap: Map<string, InspectMetricsSeries[]>;
step: InspectionStep;
metricInspectionOptions: MetricInspectionOptions;
metricInspectionAppliedOptions: MetricInspectionOptions;
timeAggregatedSeriesMap: Map<number, GraphPopoverData[]>;
}
@@ -165,7 +171,7 @@ export interface TableViewProps {
inspectMetricsTimeSeries: InspectMetricsSeries[];
setShowExpandedView: (showExpandedView: boolean) => void;
setExpandedViewOptions: (options: GraphPopoverOptions | null) => void;
metricInspectionOptions: MetricInspectionOptions;
metricInspectionAppliedOptions: MetricInspectionOptions;
isInspectMetricsRefetching: boolean;
}
@@ -174,3 +180,8 @@ export interface TableViewDataItem {
values: JSX.Element;
key: number;
}
export interface TimeSeriesLabelProps {
timeSeries: InspectMetricsSeries | undefined;
textColor: string | undefined;
}

View File

@@ -10,7 +10,7 @@ import {
GraphPopoverData,
InspectionStep,
MetricInspectionAction,
MetricInspectionOptions,
MetricInspectionState,
UseInspectMetricsReturnData,
} from './types';
import {
@@ -20,37 +20,62 @@ import {
} from './utils';
const metricInspectionReducer = (
state: MetricInspectionOptions,
state: MetricInspectionState,
action: MetricInspectionAction,
): MetricInspectionOptions => {
): MetricInspectionState => {
switch (action.type) {
case 'SET_TIME_AGGREGATION_OPTION':
return {
...state,
timeAggregationOption: action.payload,
currentOptions: {
...state.currentOptions,
timeAggregationOption: action.payload,
},
};
case 'SET_TIME_AGGREGATION_INTERVAL':
return {
...state,
timeAggregationInterval: action.payload,
currentOptions: {
...state.currentOptions,
timeAggregationInterval: action.payload,
},
};
case 'SET_SPACE_AGGREGATION_OPTION':
return {
...state,
spaceAggregationOption: action.payload,
currentOptions: {
...state.currentOptions,
spaceAggregationOption: action.payload,
},
};
case 'SET_SPACE_AGGREGATION_LABELS':
return {
...state,
spaceAggregationLabels: action.payload,
currentOptions: {
...state.currentOptions,
spaceAggregationLabels: action.payload,
},
};
case 'SET_FILTERS':
return {
...state,
filters: action.payload,
currentOptions: {
...state.currentOptions,
filters: action.payload,
},
};
case 'APPLY_METRIC_INSPECTION_OPTIONS':
return {
...state,
appliedOptions: {
...state.appliedOptions,
...state.currentOptions,
},
};
case 'RESET_INSPECTION':
return { ...INITIAL_INSPECT_METRICS_OPTIONS };
return {
...INITIAL_INSPECT_METRICS_OPTIONS,
};
default:
return state;
}
@@ -84,7 +109,7 @@ export function useInspectMetrics(
metricName: metricName ?? '',
start,
end,
filters: metricInspectionOptions.filters,
filters: metricInspectionOptions.appliedOptions.filters,
},
{
enabled: !!metricName,
@@ -117,13 +142,26 @@ export function useInspectMetrics(
);
// Evaluate inspection step
const inspectionStep = useMemo(() => {
if (metricInspectionOptions.spaceAggregationOption) {
const currentInspectionStep = useMemo(() => {
if (metricInspectionOptions.currentOptions.spaceAggregationOption) {
return InspectionStep.COMPLETED;
}
if (
metricInspectionOptions.timeAggregationOption &&
metricInspectionOptions.timeAggregationInterval
metricInspectionOptions.currentOptions.timeAggregationOption &&
metricInspectionOptions.currentOptions.timeAggregationInterval
) {
return InspectionStep.SPACE_AGGREGATION;
}
return InspectionStep.TIME_AGGREGATION;
}, [metricInspectionOptions]);
const appliedInspectionStep = useMemo(() => {
if (metricInspectionOptions.appliedOptions.spaceAggregationOption) {
return InspectionStep.COMPLETED;
}
if (
metricInspectionOptions.appliedOptions.timeAggregationOption &&
metricInspectionOptions.appliedOptions.timeAggregationInterval
) {
return InspectionStep.SPACE_AGGREGATION;
}
@@ -149,23 +187,26 @@ export function useInspectMetrics(
// Apply time aggregation once required options are set
if (
inspectionStep >= InspectionStep.SPACE_AGGREGATION &&
metricInspectionOptions.timeAggregationOption &&
metricInspectionOptions.timeAggregationInterval
appliedInspectionStep >= InspectionStep.SPACE_AGGREGATION &&
metricInspectionOptions.appliedOptions.timeAggregationOption &&
metricInspectionOptions.appliedOptions.timeAggregationInterval
) {
const {
timeAggregatedSeries,
timeAggregatedSeriesMap,
} = applyTimeAggregation(inspectMetricsTimeSeries, metricInspectionOptions);
} = applyTimeAggregation(
inspectMetricsTimeSeries,
metricInspectionOptions.appliedOptions,
);
timeSeries = timeAggregatedSeries;
setTimeAggregatedSeriesMap(timeAggregatedSeriesMap);
setAggregatedTimeSeries(timeSeries);
}
// Apply space aggregation
if (inspectionStep === InspectionStep.COMPLETED) {
if (appliedInspectionStep === InspectionStep.COMPLETED) {
const { aggregatedSeries, spaceAggregatedSeriesMap } = applySpaceAggregation(
timeSeries,
metricInspectionOptions,
metricInspectionOptions.appliedOptions,
);
timeSeries = aggregatedSeries;
setSpaceAggregatedSeriesMap(spaceAggregatedSeriesMap);
@@ -186,7 +227,7 @@ export function useInspectMetrics(
const rawData = [timestamps, ...timeseriesArray];
return rawData.map((series) => new Float64Array(series));
}, [inspectMetricsTimeSeries, inspectionStep, metricInspectionOptions]);
}, [inspectMetricsTimeSeries, appliedInspectionStep, metricInspectionOptions]);
const spaceAggregationLabels = useMemo(() => {
const labels = new Set<string>();
@@ -216,7 +257,7 @@ export function useInspectMetrics(
spaceAggregationLabels,
metricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep,
inspectionStep: currentInspectionStep,
isInspectMetricsRefetching,
spaceAggregatedSeriesMap,
aggregatedTimeSeries,

View File

@@ -1,36 +1,12 @@
/* eslint-disable no-nested-ternary */
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 classNames from 'classnames';
import { initialQueriesMap } from 'constants/queryBuilder';
import { AggregatorFilter } from 'container/QueryBuilder/filters';
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { HardHat } from 'lucide-react';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
import {
SPACE_AGGREGATION_OPTIONS,
TIME_AGGREGATION_OPTIONS,
} from './constants';
import {
GraphPopoverData,
GraphPopoverOptions,
InspectionStep,
MetricFiltersProps,
MetricInspectionOptions,
MetricNameSearchProps,
MetricSpaceAggregationProps,
MetricTimeAggregationProps,
SpaceAggregationOptions,
TimeAggregationOptions,
} from './types';
@@ -72,220 +48,6 @@ export function getDefaultTimeAggregationInterval(
return Math.max(60, reportingInterval);
}
export function MetricNameSearch({
metricName,
setMetricName,
}: MetricNameSearchProps): JSX.Element {
const [searchText, setSearchText] = useState(metricName);
const handleSetMetricName = (value: BaseAutocompleteData): void => {
setMetricName(value.key);
};
const handleChange = (value: BaseAutocompleteData): void => {
setSearchText(value.key);
};
return (
<div
data-testid="metric-name-search"
className="inspect-metrics-input-group metric-name-search"
>
<Typography.Text>From</Typography.Text>
<AggregatorFilter
defaultValue={searchText ?? ''}
query={initialQueriesMap[DataSource.METRICS].builder.queryData[0]}
onSelect={handleSetMetricName}
onChange={handleChange}
/>
</div>
);
}
export function MetricFilters({
dispatchMetricInspectionOptions,
searchQuery,
metricName,
metricType,
}: MetricFiltersProps): JSX.Element {
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: searchQuery,
entityVersion: '',
});
const aggregateAttribute = useMemo(
() => ({
key: metricName ?? '',
dataType: DataTypes.String,
type: metricType,
id: `${metricName}--${DataTypes.String}--${metricType}--true`,
}),
[metricName, metricType],
);
return (
<div
data-testid="metric-filters"
className="inspect-metrics-input-group metric-filters"
>
<Typography.Text>Where</Typography.Text>
<QueryBuilderSearch
query={{
...searchQuery,
aggregateAttribute,
}}
onChange={(value): void => {
handleChangeQueryData('filters', value);
logEvent(MetricsExplorerEvents.FilterApplied, {
[MetricsExplorerEventKeys.Modal]: 'inspect',
});
dispatchMetricInspectionOptions({
type: 'SET_FILTERS',
payload: value,
});
}}
suffixIcon={<HardHat size={16} />}
disableNavigationShortcuts
/>
</div>
);
}
export function MetricTimeAggregation({
metricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep,
inspectMetricsTimeSeries,
}: MetricTimeAggregationProps): JSX.Element {
return (
<div
data-testid="metric-time-aggregation"
className="metric-time-aggregation"
>
<div
className={classNames('metric-time-aggregation-header', {
'selected-step': inspectionStep === InspectionStep.TIME_AGGREGATION,
})}
>
<Typography.Text>AGGREGATE BY TIME</Typography.Text>
</div>
<div className="metric-time-aggregation-content">
<div className="inspect-metrics-input-group">
<Typography.Text>Align with</Typography.Text>
<Select
value={metricInspectionOptions.timeAggregationOption}
onChange={(value): void => {
dispatchMetricInspectionOptions({
type: 'SET_TIME_AGGREGATION_OPTION',
payload: value,
});
// set the time aggregation interval to the default value if it is not set
if (!metricInspectionOptions.timeAggregationInterval) {
dispatchMetricInspectionOptions({
type: 'SET_TIME_AGGREGATION_INTERVAL',
payload: getDefaultTimeAggregationInterval(
inspectMetricsTimeSeries[0],
),
});
}
}}
style={{ width: 130 }}
placeholder="Select option"
>
{Object.entries(TIME_AGGREGATION_OPTIONS).map(([key, value]) => (
<Select.Option key={key} value={key}>
{value}
</Select.Option>
))}
</Select>
</div>
<div className="inspect-metrics-input-group">
<Typography.Text>aggregated every</Typography.Text>
<Input
type="number"
className="no-arrows-input"
value={metricInspectionOptions.timeAggregationInterval}
placeholder="Select interval..."
suffix="seconds"
onChange={(e): void => {
dispatchMetricInspectionOptions({
type: 'SET_TIME_AGGREGATION_INTERVAL',
payload: parseInt(e.target.value, 10),
});
}}
onWheel={(e): void => (e.target as HTMLInputElement).blur()}
/>
</div>
</div>
</div>
);
}
export function MetricSpaceAggregation({
spaceAggregationLabels,
metricInspectionOptions,
dispatchMetricInspectionOptions,
inspectionStep,
}: MetricSpaceAggregationProps): JSX.Element {
return (
<div
data-testid="metric-space-aggregation"
className="metric-space-aggregation"
>
<div
className={classNames('metric-space-aggregation-header', {
'selected-step': inspectionStep === InspectionStep.SPACE_AGGREGATION,
})}
>
<Typography.Text>AGGREGATE BY LABELS</Typography.Text>
</div>
<div className="metric-space-aggregation-content">
<div className="metric-space-aggregation-content-left">
<Select
value={metricInspectionOptions.spaceAggregationOption}
placeholder="Select option"
onChange={(value): void => {
dispatchMetricInspectionOptions({
type: 'SET_SPACE_AGGREGATION_OPTION',
payload: value,
});
}}
style={{ width: 130 }}
disabled={inspectionStep === InspectionStep.TIME_AGGREGATION}
>
{/* eslint-disable-next-line sonarjs/no-identical-functions */}
{Object.entries(SPACE_AGGREGATION_OPTIONS).map(([key, value]) => (
<Select.Option key={key} value={key}>
{value}
</Select.Option>
))}
</Select>
</div>
<Select
mode="multiple"
style={{ width: '100%' }}
placeholder="Search for attributes..."
value={metricInspectionOptions.spaceAggregationLabels}
onChange={(value): void => {
dispatchMetricInspectionOptions({
type: 'SET_SPACE_AGGREGATION_LABELS',
payload: value,
});
}}
disabled={inspectionStep === InspectionStep.TIME_AGGREGATION}
>
{spaceAggregationLabels.map((label) => (
<Select.Option key={label} value={label}>
{label}
</Select.Option>
))}
</Select>
</div>
</div>
);
}
export function applyFilters(
inspectMetricsTimeSeries: InspectMetricsSeries[],
filters: TagFilter,
@@ -322,7 +84,7 @@ export function applyFilters(
export function applyTimeAggregation(
inspectMetricsTimeSeries: InspectMetricsSeries[],
metricInspectionOptions: MetricInspectionOptions,
metricInspectionAppliedOptions: MetricInspectionOptions,
): {
timeAggregatedSeries: InspectMetricsSeries[];
timeAggregatedSeriesMap: Map<number, GraphPopoverData[]>;
@@ -330,7 +92,7 @@ export function applyTimeAggregation(
const {
timeAggregationOption,
timeAggregationInterval,
} = metricInspectionOptions;
} = metricInspectionAppliedOptions;
if (!timeAggregationInterval) {
return {
@@ -415,7 +177,7 @@ export function applyTimeAggregation(
export function applySpaceAggregation(
inspectMetricsTimeSeries: InspectMetricsSeries[],
metricInspectionOptions: MetricInspectionOptions,
metricInspectionAppliedOptions: MetricInspectionOptions,
): {
aggregatedSeries: InspectMetricsSeries[];
spaceAggregatedSeriesMap: Map<string, InspectMetricsSeries[]>;
@@ -425,7 +187,7 @@ export function applySpaceAggregation(
inspectMetricsTimeSeries.forEach((series) => {
// Create composite key from selected labels
const key = metricInspectionOptions.spaceAggregationLabels
const key = metricInspectionAppliedOptions.spaceAggregationLabels
.map((label) => `${label}:${series.labels[label]}`)
.join(',');
@@ -460,7 +222,7 @@ export function applySpaceAggregation(
([timestamp, values]) => {
let aggregatedValue: number;
switch (metricInspectionOptions.spaceAggregationOption) {
switch (metricInspectionAppliedOptions.spaceAggregationOption) {
case SpaceAggregationOptions.SUM_BY:
aggregatedValue = values.reduce((sum, val) => sum + val, 0);
break;
@@ -695,103 +457,6 @@ export const formatTimestampToFullDateTime = (
return `${datePart}${timePart}`;
};
export function getTimeSeriesLabel(
timeSeries: InspectMetricsSeries | null,
textColor: string | undefined,
): JSX.Element {
return (
<>
{Object.entries(timeSeries?.labels ?? {}).map(([key, value]) => (
<span key={key}>
<Typography.Text style={{ color: textColor, fontWeight: 600 }}>
{key}
</Typography.Text>
: {value}{' '}
</span>
))}
</>
);
}
export function HoverPopover({
options,
step,
metricInspectionOptions,
}: {
options: GraphPopoverOptions;
step: InspectionStep;
metricInspectionOptions: MetricInspectionOptions;
}): JSX.Element {
const closestTimestamp = useMemo(() => {
if (!options.timeSeries) {
return options.timestamp;
}
return options.timeSeries?.values.reduce((prev, curr) => {
const prevDiff = Math.abs(prev.timestamp - options.timestamp);
const currDiff = Math.abs(curr.timestamp - options.timestamp);
return prevDiff < currDiff ? prev : curr;
}).timestamp;
}, [options.timeSeries, options.timestamp]);
const closestValue = useMemo(() => {
if (!options.timeSeries) {
return options.value;
}
const index = options.timeSeries.values.findIndex(
(value) => value.timestamp === closestTimestamp,
);
return index !== undefined && index >= 0
? options.timeSeries?.values[index].value
: null;
}, [options.timeSeries, closestTimestamp, options.value]);
const title = useMemo(() => {
if (
step === InspectionStep.COMPLETED &&
metricInspectionOptions.spaceAggregationLabels.length === 0
) {
return undefined;
}
if (step === InspectionStep.COMPLETED && options.timeSeries?.title) {
return options.timeSeries.title;
}
if (!options.timeSeries) {
return undefined;
}
return getTimeSeriesLabel(
options.timeSeries,
options.timeSeries?.strokeColor,
);
}, [step, options.timeSeries, metricInspectionOptions]);
return (
<Card
className="hover-popover-card"
style={{
top: options.y + 10,
left: options.x + 10,
}}
>
<div className="hover-popover-row">
<Typography.Text>
{formatTimestampToFullDateTime(closestTimestamp ?? 0)}
</Typography.Text>
<Typography.Text>{Number(closestValue).toFixed(2)}</Typography.Text>
</div>
{options.timeSeries && (
<Typography.Text
style={{
color: options.timeSeries?.strokeColor,
fontWeight: 200,
}}
>
{title}
</Typography.Text>
)}
</Card>
);
}
export function onGraphHover(
e: MouseEvent,
u: uPlot,

View File

@@ -333,7 +333,7 @@ function Summary(): JSX.Element {
openInspectModal={openInspectModal}
/>
)}
{isInspectModalOpen && (
{isInspectModalOpen && selectedMetricName && (
<InspectModal
isOpen={isInspectModalOpen}
onClose={closeInspectModal}

View File

@@ -513,7 +513,7 @@ func (m *module) fetchTimeseriesMetadata(ctx context.Context, orgID valuer.UUID,
"metric_name",
"anyLast(description) AS description",
"anyLast(type) AS metric_type",
"anyLast(unit) AS metric_unit",
"argMax(unit, unix_milli) AS metric_unit",
"anyLast(temporality) AS temporality",
"anyLast(is_monotonic) AS is_monotonic",
)

View File

@@ -5441,7 +5441,7 @@ func (r *ClickHouseReader) ListSummaryMetrics(ctx context.Context, orgID valuer.
t.metric_name AS metric_name,
ANY_VALUE(t.description) AS description,
ANY_VALUE(t.type) AS metric_type,
ANY_VALUE(t.unit) AS metric_unit,
argMax(t.unit, unix_milli) AS metric_unit,
uniq(t.fingerprint) AS timeseries,
uniq(metric_name) OVER() AS total
FROM %s.%s AS t

View File

@@ -1,86 +0,0 @@
package formatter
import (
"fmt"
"github.com/SigNoz/signoz/pkg/query-service/converter"
"github.com/dustin/go-humanize"
)
type dataFormatter struct {
}
func NewDataFormatter() Formatter {
return &dataFormatter{}
}
func (*dataFormatter) Name() string {
return "data"
}
func (f *dataFormatter) Format(value float64, unit string) string {
switch unit {
case "bytes", "By":
return humanize.IBytes(uint64(value))
case "decbytes":
return humanize.Bytes(uint64(value))
case "bits", "bit":
// humanize.IBytes/Bytes doesn't support bits
// and returns 0 B for values less than a byte
if value < 8 {
return fmt.Sprintf("%v b", value)
}
return humanize.IBytes(uint64(value / 8))
case "decbits":
if value < 8 {
return fmt.Sprintf("%v b", value)
}
return humanize.Bytes(uint64(value / 8))
case "kbytes", "KiBy":
return humanize.IBytes(uint64(value * converter.Kibibit))
case "Kibit":
return humanize.IBytes(uint64(value * converter.Kibibit / 8))
case "decKbytes", "deckbytes", "kBy":
return humanize.Bytes(uint64(value * converter.Kilobit))
case "kbit":
return humanize.Bytes(uint64(value * converter.Kilobit / 8))
case "mbytes", "MiBy":
return humanize.IBytes(uint64(value * converter.Mebibit))
case "Mibit":
return humanize.IBytes(uint64(value * converter.Mebibit / 8))
case "decMbytes", "decmbytes", "MBy":
return humanize.Bytes(uint64(value * converter.Megabit))
case "Mbit":
return humanize.Bytes(uint64(value * converter.Megabit / 8))
case "gbytes", "GiBy":
return humanize.IBytes(uint64(value * converter.Gibibit))
case "Gibit":
return humanize.IBytes(uint64(value * converter.Gibibit / 8))
case "decGbytes", "decgbytes", "GBy":
return humanize.Bytes(uint64(value * converter.Gigabit))
case "Gbit":
return humanize.Bytes(uint64(value * converter.Gigabit / 8))
case "tbytes", "TiBy":
return humanize.IBytes(uint64(value * converter.Tebibit))
case "Tibit":
return humanize.IBytes(uint64(value * converter.Tebibit / 8))
case "decTbytes", "dectbytes", "TBy":
return humanize.Bytes(uint64(value * converter.Terabit))
case "Tbit":
return humanize.Bytes(uint64(value * converter.Terabit / 8))
case "pbytes", "PiBy":
return humanize.IBytes(uint64(value * converter.Pebibit))
case "Pbit":
return humanize.Bytes(uint64(value * converter.Petabit / 8))
case "decPbytes", "decpbytes", "PBy":
return humanize.Bytes(uint64(value * converter.Petabit))
case "EiBy":
return humanize.IBytes(uint64(value * converter.Exbibit))
case "Ebit":
return humanize.Bytes(uint64(value * converter.Exabit / 8))
case "EBy":
return humanize.Bytes(uint64(value * converter.Exabit))
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)
}

View File

@@ -1,91 +0,0 @@
package formatter
import (
"fmt"
"github.com/SigNoz/signoz/pkg/query-service/converter"
"github.com/dustin/go-humanize"
)
type dataRateFormatter struct {
}
func NewDataRateFormatter() Formatter {
return &dataRateFormatter{}
}
func (*dataRateFormatter) Name() string {
return "data_rate"
}
func (f *dataRateFormatter) Format(value float64, unit string) string {
switch unit {
case "binBps":
return humanize.IBytes(uint64(value)) + "/s"
case "Bps", "By/s":
return humanize.Bytes(uint64(value)) + "/s"
case "binbps":
// humanize.IBytes/Bytes doesn't support bits
// and returns 0 B for values less than a byte
if value < 8 {
return fmt.Sprintf("%v b/s", value)
}
return humanize.IBytes(uint64(value/8)) + "/s"
case "bps", "bit/s":
if value < 8 {
return fmt.Sprintf("%v b/s", value)
}
return humanize.Bytes(uint64(value/8)) + "/s"
case "KiBs", "KiBy/s":
return humanize.IBytes(uint64(value*converter.KibibitPerSecond)) + "/s"
case "Kibits", "Kibit/s":
return humanize.IBytes(uint64(value*converter.KibibitPerSecond/8)) + "/s"
case "KBs", "kBy/s":
return humanize.IBytes(uint64(value*converter.KilobitPerSecond)) + "/s"
case "Kbits", "kbit/s":
return humanize.Bytes(uint64(value*converter.KilobitPerSecond/8)) + "/s"
case "MiBs", "MiBy/s":
return humanize.IBytes(uint64(value*converter.MebibitPerSecond)) + "/s"
case "Mibits", "Mibit/s":
return humanize.IBytes(uint64(value*converter.MebibitPerSecond/8)) + "/s"
case "MBs", "MBy/s":
return humanize.IBytes(uint64(value*converter.MegabitPerSecond)) + "/s"
case "Mbits", "Mbit/s":
return humanize.Bytes(uint64(value*converter.MegabitPerSecond/8)) + "/s"
case "GiBs", "GiBy/s":
return humanize.IBytes(uint64(value*converter.GibibitPerSecond)) + "/s"
case "Gibits", "Gibit/s":
return humanize.IBytes(uint64(value*converter.GibibitPerSecond/8)) + "/s"
case "GBs", "GBy/s":
return humanize.IBytes(uint64(value*converter.GigabitPerSecond)) + "/s"
case "Gbits", "Gbit/s":
return humanize.Bytes(uint64(value*converter.GigabitPerSecond/8)) + "/s"
case "TiBs", "TiBy/s":
return humanize.IBytes(uint64(value*converter.TebibitPerSecond)) + "/s"
case "Tibits", "Tibit/s":
return humanize.IBytes(uint64(value*converter.TebibitPerSecond/8)) + "/s"
case "TBs", "TBy/s":
return humanize.IBytes(uint64(value*converter.TerabitPerSecond)) + "/s"
case "Tbits", "Tbit/s":
return humanize.Bytes(uint64(value*converter.TerabitPerSecond/8)) + "/s"
case "PiBs", "PiBy/s":
return humanize.IBytes(uint64(value*converter.PebibitPerSecond)) + "/s"
case "Pibits", "Pibit/s":
return humanize.IBytes(uint64(value*converter.PebibitPerSecond/8)) + "/s"
case "PBs", "PBy/s":
return humanize.IBytes(uint64(value*converter.PetabitPerSecond)) + "/s"
case "Pbits", "Pbit/s":
return humanize.Bytes(uint64(value*converter.PetabitPerSecond/8)) + "/s"
// Exa units
case "EBy/s":
return humanize.Bytes(uint64(value*converter.ExabitPerSecond)) + "/s"
case "Ebit/s":
return humanize.Bytes(uint64(value*converter.ExabitPerSecond/8)) + "/s"
case "EiBy/s":
return humanize.IBytes(uint64(value*converter.ExbibitPerSecond)) + "/s"
case "Eibit/s":
return humanize.IBytes(uint64(value*converter.ExbibitPerSecond/8)) + "/s"
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)
}

View File

@@ -9,7 +9,7 @@ import (
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/prometheus"
"github.com/SigNoz/signoz/pkg/query-service/formatter"
"github.com/SigNoz/signoz/pkg/units"
"github.com/SigNoz/signoz/pkg/query-service/interfaces"
"github.com/SigNoz/signoz/pkg/query-service/model"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
@@ -185,7 +185,7 @@ func (r *PromRule) buildAndRunQuery(ctx context.Context, ts time.Time) (ruletype
func (r *PromRule) Eval(ctx context.Context, ts time.Time) (int, error) {
prevState := r.State()
valueFormatter := formatter.FromUnit(r.Unit())
valueFormatter := units.FormatterFromUnit(r.Unit())
// prepare query, run query get data and filter the data based on the threshold
results, err := r.buildAndRunQuery(ctx, ts)

View File

@@ -33,7 +33,7 @@ import (
logsv3 "github.com/SigNoz/signoz/pkg/query-service/app/logs/v3"
tracesV4 "github.com/SigNoz/signoz/pkg/query-service/app/traces/v4"
"github.com/SigNoz/signoz/pkg/query-service/formatter"
"github.com/SigNoz/signoz/pkg/units"
querierV5 "github.com/SigNoz/signoz/pkg/querier"
@@ -571,7 +571,7 @@ func (r *ThresholdRule) buildAndRunQueryV5(ctx context.Context, orgID valuer.UUI
func (r *ThresholdRule) Eval(ctx context.Context, ts time.Time) (int, error) {
prevState := r.State()
valueFormatter := formatter.FromUnit(r.Unit())
valueFormatter := units.FormatterFromUnit(r.Unit())
var res ruletypes.Vector
var err error

View File

@@ -7,7 +7,7 @@ import (
"strings"
"github.com/SigNoz/signoz/pkg/errors"
"github.com/SigNoz/signoz/pkg/query-service/converter"
"github.com/SigNoz/signoz/pkg/units"
v3 "github.com/SigNoz/signoz/pkg/query-service/model/v3"
"github.com/SigNoz/signoz/pkg/query-service/utils/labels"
"github.com/SigNoz/signoz/pkg/valuer"
@@ -201,12 +201,12 @@ func sortThresholds(thresholds []BasicRuleThreshold) {
// convertToRuleUnit converts the given value from the target unit to the rule unit
func (b BasicRuleThreshold) convertToRuleUnit(val float64, ruleUnit string) float64 {
unitConverter := converter.FromUnit(converter.Unit(b.TargetUnit))
unitConverter := units.ConverterFromUnit(units.Unit(b.TargetUnit))
// convert the target value to the y-axis unit
value := unitConverter.Convert(converter.Value{
value := unitConverter.Convert(units.Value{
F: val,
U: converter.Unit(b.TargetUnit),
}, converter.Unit(ruleUnit))
U: units.Unit(b.TargetUnit),
}, units.Unit(ruleUnit))
return value.F
}

View File

@@ -1,4 +1,4 @@
package converter
package units
// Unit represents a unit of measurement
type Unit string
@@ -40,8 +40,8 @@ var (
NoneConverter = &noneConverter{}
)
// FromUnit returns a converter for the given unit
func FromUnit(u Unit) Converter {
// ConverterFromUnit returns a converter for the given unit
func ConverterFromUnit(u Unit) Converter {
switch u {
case "ns", "us", "µs", "ms", "s", "m", "h", "d", "min", "w", "wk":
return DurationConverter

View File

@@ -1,4 +1,4 @@
package converter
package units
// boolConverter is a Converter implementation for bool
type boolConverter struct{}

View File

@@ -1,4 +1,4 @@
package converter
package units
const (
// base 10 (SI prefixes)

View File

@@ -1,4 +1,4 @@
package converter
package units
const (
// base 10 (SI prefixes)

View File

@@ -1,4 +1,4 @@
package converter
package units
import (
"testing"

View File

@@ -1,4 +1,4 @@
package converter
package units
import (
"testing"

View File

@@ -1,4 +1,4 @@
package converter
package units
// percentConverter is a converter for percent unit
type percentConverter struct{}

View File

@@ -1,4 +1,4 @@
package converter
package units
import (
"testing"

View File

@@ -1,4 +1,4 @@
package converter
package units
// throughputConverter is an implementation of Converter that converts throughput
type throughputConverter struct {

View File

@@ -1,4 +1,4 @@
package converter
package units
type Duration float64

View File

@@ -1,4 +1,4 @@
package converter
package units
import (
"testing"

View File

@@ -1,4 +1,4 @@
package formatter
package units
type Formatter interface {
Format(value float64, unit string) string
@@ -16,7 +16,7 @@ var (
ThroughputFormatter = NewThroughputFormatter()
)
func FromUnit(u string) Formatter {
func FormatterFromUnit(u string) Formatter {
switch u {
case "ns", "us", "µs", "ms", "s", "m", "h", "d", "min", "w", "wk":
return DurationFormatter

View File

@@ -1,4 +1,4 @@
package formatter
package units
import "fmt"

View File

@@ -0,0 +1,85 @@
package units
import (
"fmt"
"github.com/dustin/go-humanize"
)
type dataFormatter struct {
}
func NewDataFormatter() Formatter {
return &dataFormatter{}
}
func (*dataFormatter) Name() string {
return "data"
}
func (f *dataFormatter) Format(value float64, unit string) string {
switch unit {
case "bytes", "By":
return humanize.IBytes(uint64(value))
case "decbytes":
return humanize.Bytes(uint64(value))
case "bits", "bit":
// humanize.IBytes/Bytes doesn't support bits
// and returns 0 B for values less than a byte
if value < 8 {
return fmt.Sprintf("%v b", value)
}
return humanize.IBytes(uint64(value / 8))
case "decbits":
if value < 8 {
return fmt.Sprintf("%v b", value)
}
return humanize.Bytes(uint64(value / 8))
case "kbytes", "KiBy":
return humanize.IBytes(uint64(value * Kibibit))
case "Kibit":
return humanize.IBytes(uint64(value * Kibibit / 8))
case "decKbytes", "deckbytes", "kBy":
return humanize.Bytes(uint64(value * Kilobit))
case "kbit":
return humanize.Bytes(uint64(value * Kilobit / 8))
case "mbytes", "MiBy":
return humanize.IBytes(uint64(value * Mebibit))
case "Mibit":
return humanize.IBytes(uint64(value * Mebibit / 8))
case "decMbytes", "decmbytes", "MBy":
return humanize.Bytes(uint64(value * Megabit))
case "Mbit":
return humanize.Bytes(uint64(value * Megabit / 8))
case "gbytes", "GiBy":
return humanize.IBytes(uint64(value * Gibibit))
case "Gibit":
return humanize.IBytes(uint64(value * Gibibit / 8))
case "decGbytes", "decgbytes", "GBy":
return humanize.Bytes(uint64(value * Gigabit))
case "Gbit":
return humanize.Bytes(uint64(value * Gigabit / 8))
case "tbytes", "TiBy":
return humanize.IBytes(uint64(value * Tebibit))
case "Tibit":
return humanize.IBytes(uint64(value * Tebibit / 8))
case "decTbytes", "dectbytes", "TBy":
return humanize.Bytes(uint64(value * Terabit))
case "Tbit":
return humanize.Bytes(uint64(value * Terabit / 8))
case "pbytes", "PiBy":
return humanize.IBytes(uint64(value * Pebibit))
case "Pbit":
return humanize.Bytes(uint64(value * Petabit / 8))
case "decPbytes", "decpbytes", "PBy":
return humanize.Bytes(uint64(value * Petabit))
case "EiBy":
return humanize.IBytes(uint64(value * Exbibit))
case "Ebit":
return humanize.Bytes(uint64(value * Exabit / 8))
case "EBy":
return humanize.Bytes(uint64(value * Exabit))
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)
}

View File

@@ -0,0 +1,90 @@
package units
import (
"fmt"
"github.com/dustin/go-humanize"
)
type dataRateFormatter struct {
}
func NewDataRateFormatter() Formatter {
return &dataRateFormatter{}
}
func (*dataRateFormatter) Name() string {
return "data_rate"
}
func (f *dataRateFormatter) Format(value float64, unit string) string {
switch unit {
case "binBps":
return humanize.IBytes(uint64(value)) + "/s"
case "Bps", "By/s":
return humanize.Bytes(uint64(value)) + "/s"
case "binbps":
// humanize.IBytes/Bytes doesn't support bits
// and returns 0 B for values less than a byte
if value < 8 {
return fmt.Sprintf("%v b/s", value)
}
return humanize.IBytes(uint64(value/8)) + "/s"
case "bps", "bit/s":
if value < 8 {
return fmt.Sprintf("%v b/s", value)
}
return humanize.Bytes(uint64(value/8)) + "/s"
case "KiBs", "KiBy/s":
return humanize.IBytes(uint64(value*KibibitPerSecond)) + "/s"
case "Kibits", "Kibit/s":
return humanize.IBytes(uint64(value*KibibitPerSecond/8)) + "/s"
case "KBs", "kBy/s":
return humanize.IBytes(uint64(value*KilobitPerSecond)) + "/s"
case "Kbits", "kbit/s":
return humanize.Bytes(uint64(value*KilobitPerSecond/8)) + "/s"
case "MiBs", "MiBy/s":
return humanize.IBytes(uint64(value*MebibitPerSecond)) + "/s"
case "Mibits", "Mibit/s":
return humanize.IBytes(uint64(value*MebibitPerSecond/8)) + "/s"
case "MBs", "MBy/s":
return humanize.IBytes(uint64(value*MegabitPerSecond)) + "/s"
case "Mbits", "Mbit/s":
return humanize.Bytes(uint64(value*MegabitPerSecond/8)) + "/s"
case "GiBs", "GiBy/s":
return humanize.IBytes(uint64(value*GibibitPerSecond)) + "/s"
case "Gibits", "Gibit/s":
return humanize.IBytes(uint64(value*GibibitPerSecond/8)) + "/s"
case "GBs", "GBy/s":
return humanize.IBytes(uint64(value*GigabitPerSecond)) + "/s"
case "Gbits", "Gbit/s":
return humanize.Bytes(uint64(value*GigabitPerSecond/8)) + "/s"
case "TiBs", "TiBy/s":
return humanize.IBytes(uint64(value*TebibitPerSecond)) + "/s"
case "Tibits", "Tibit/s":
return humanize.IBytes(uint64(value*TebibitPerSecond/8)) + "/s"
case "TBs", "TBy/s":
return humanize.IBytes(uint64(value*TerabitPerSecond)) + "/s"
case "Tbits", "Tbit/s":
return humanize.Bytes(uint64(value*TerabitPerSecond/8)) + "/s"
case "PiBs", "PiBy/s":
return humanize.IBytes(uint64(value*PebibitPerSecond)) + "/s"
case "Pibits", "Pibit/s":
return humanize.IBytes(uint64(value*PebibitPerSecond/8)) + "/s"
case "PBs", "PBy/s":
return humanize.IBytes(uint64(value*PetabitPerSecond)) + "/s"
case "Pbits", "Pbit/s":
return humanize.Bytes(uint64(value*PetabitPerSecond/8)) + "/s"
// Exa units
case "EBy/s":
return humanize.Bytes(uint64(value*ExabitPerSecond)) + "/s"
case "Ebit/s":
return humanize.Bytes(uint64(value*ExabitPerSecond/8)) + "/s"
case "EiBy/s":
return humanize.IBytes(uint64(value*ExbibitPerSecond)) + "/s"
case "Eibit/s":
return humanize.IBytes(uint64(value*ExbibitPerSecond/8)) + "/s"
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)
}

View File

@@ -1,4 +1,4 @@
package formatter
package units
import (
"testing"

View File

@@ -1,4 +1,4 @@
package formatter
package units
import (
"testing"
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestData(t *testing.T) {
func TestFormatterData(t *testing.T) {
dataFormatter := NewDataFormatter()
assert.Equal(t, "1 B", dataFormatter.Format(1, "bytes"))

View File

@@ -1,4 +1,4 @@
package formatter
package units
import "fmt"

View File

@@ -1,4 +1,4 @@
package formatter
package units
import "fmt"

View File

@@ -1,4 +1,4 @@
package formatter
package units
import (
"fmt"
@@ -14,36 +14,36 @@ type IntervalsInSecondsType map[Interval]int
type Interval string
const (
Year Interval = "year"
Month Interval = "month"
Week Interval = "week"
Day Interval = "day"
Hour Interval = "hour"
Minute Interval = "minute"
Second Interval = "second"
Millisecond Interval = "millisecond"
IntervalYear Interval = "year"
IntervalMonth Interval = "month"
IntervalWeek Interval = "week"
IntervalDay Interval = "day"
IntervalHour Interval = "hour"
IntervalMinute Interval = "minute"
IntervalSecond Interval = "second"
IntervalMillisecond Interval = "millisecond"
)
var Units = []Interval{
Year,
Month,
Week,
Day,
Hour,
Minute,
Second,
Millisecond,
IntervalYear,
IntervalMonth,
IntervalWeek,
IntervalDay,
IntervalHour,
IntervalMinute,
IntervalSecond,
IntervalMillisecond,
}
var IntervalsInSeconds = IntervalsInSecondsType{
Year: 31536000,
Month: 2592000,
Week: 604800,
Day: 86400,
Hour: 3600,
Minute: 60,
Second: 1,
Millisecond: 1,
IntervalYear: 31536000,
IntervalMonth: 2592000,
IntervalWeek: 604800,
IntervalDay: 86400,
IntervalHour: 3600,
IntervalMinute: 60,
IntervalSecond: 1,
IntervalMillisecond: 1,
}
type DecimalCount *int
@@ -78,9 +78,8 @@ func toFixed(value float64, decimals DecimalCount) string {
}
decimalPos := strings.Index(formatted, ".")
precision := 0
if decimalPos != -1 {
precision = len(formatted) - decimalPos - 1
precision := len(formatted) - decimalPos - 1
if precision < *decimals {
return formatted + strings.Repeat("0", *decimals-precision)
}
@@ -89,8 +88,8 @@ func toFixed(value float64, decimals DecimalCount) string {
return formatted
}
func toFixedScaled(value float64, decimals DecimalCount, scaleFormat string) string {
return toFixed(value, decimals) + scaleFormat
func toFixedScaled(value float64, scaleFormat string) string {
return toFixed(value, nil) + scaleFormat
}
func getDecimalsForValue(value float64) int {

View File

@@ -1,4 +1,4 @@
package formatter
package units
import (
"testing"

View File

@@ -1,4 +1,4 @@
package formatter
package units
import "fmt"
@@ -13,35 +13,35 @@ func (*throughputFormatter) Name() string {
return "throughput"
}
func simpleCountUnit(value float64, decimals *int, symbol string) string {
func simpleCountUnit(value float64, symbol string) string {
units := []string{"", "K", "M", "B", "T"}
scaler := scaledUnits(1000, units, 0)
return scaler(value, decimals) + " " + symbol
return scaler(value, nil) + " " + symbol
}
func (f *throughputFormatter) Format(value float64, unit string) string {
switch unit {
case "cps", "{count}/s":
return simpleCountUnit(value, nil, "c/s")
return simpleCountUnit(value, "c/s")
case "ops", "{ops}/s":
return simpleCountUnit(value, nil, "op/s")
return simpleCountUnit(value, "op/s")
case "reqps", "{req}/s":
return simpleCountUnit(value, nil, "req/s")
return simpleCountUnit(value, "req/s")
case "rps", "{read}/s":
return simpleCountUnit(value, nil, "r/s")
return simpleCountUnit(value, "r/s")
case "wps", "{write}/s":
return simpleCountUnit(value, nil, "w/s")
return simpleCountUnit(value, "w/s")
case "iops", "{iops}/s":
return simpleCountUnit(value, nil, "iops")
return simpleCountUnit(value, "iops")
case "cpm", "{count}/min":
return simpleCountUnit(value, nil, "c/m")
return simpleCountUnit(value, "c/m")
case "opm", "{ops}/min":
return simpleCountUnit(value, nil, "op/m")
return simpleCountUnit(value, "op/m")
case "rpm", "{read}/min":
return simpleCountUnit(value, nil, "r/m")
return simpleCountUnit(value, "r/m")
case "wpm", "{write}/min":
return simpleCountUnit(value, nil, "w/m")
return simpleCountUnit(value, "w/m")
}
// When unit is not matched, return the value as it is.
return fmt.Sprintf("%v", value)

View File

@@ -1,4 +1,4 @@
package formatter
package units
import (
"testing"

View File

@@ -1,4 +1,4 @@
package formatter
package units
import (
"fmt"
@@ -46,17 +46,17 @@ func toNanoSeconds(value float64) string {
if absValue < 1000 {
return toFixed(value, nil) + " ns"
} else if absValue < 1000000 { // 2000 ns is better represented as 2 µs
return toFixedScaled(value/1000, nil, " µs")
return toFixedScaled(value/1000, " µs")
} else if absValue < 1000000000 { // 2000000 ns is better represented as 2 ms
return toFixedScaled(value/1000000, nil, " ms")
return toFixedScaled(value/1000000, " ms")
} else if absValue < 60000000000 {
return toFixedScaled(value/1000000000, nil, " s")
return toFixedScaled(value/1000000000, " s")
} else if absValue < 3600000000000 {
return toFixedScaled(value/60000000000, nil, " min")
return toFixedScaled(value/60000000000, " min")
} else if absValue < 86400000000000 {
return toFixedScaled(value/3600000000000, nil, " hour")
return toFixedScaled(value/3600000000000, " hour")
} else {
return toFixedScaled(value/86400000000000, nil, " day")
return toFixedScaled(value/86400000000000, " day")
}
}
@@ -66,9 +66,9 @@ func toMicroSeconds(value float64) string {
if absValue < 1000 {
return toFixed(value, nil) + " µs"
} else if absValue < 1000000 { // 2000 µs is better represented as 2 ms
return toFixedScaled(value/1000, nil, " ms")
return toFixedScaled(value/1000, " ms")
} else {
return toFixedScaled(value/1000000, nil, " s")
return toFixedScaled(value/1000000, " s")
}
}
@@ -80,16 +80,16 @@ func toMilliSeconds(value float64) string {
if absValue < 1000 {
return toFixed(value, nil) + " ms"
} else if absValue < 60000 {
return toFixedScaled(value/1000, nil, " s")
return toFixedScaled(value/1000, " s")
} else if absValue < 3600000 {
return toFixedScaled(value/60000, nil, " min")
return toFixedScaled(value/60000, " min")
} else if absValue < 86400000 { // 172800000 ms is better represented as 2 day
return toFixedScaled(value/3600000, nil, " hour")
return toFixedScaled(value/3600000, " hour")
} else if absValue < 31536000000 {
return toFixedScaled(value/86400000, nil, " day")
return toFixedScaled(value/86400000, " day")
}
return toFixedScaled(value/31536000000, nil, " year")
return toFixedScaled(value/31536000000, " year")
}
// toSeconds returns a easy to read string representation of the given value in seconds
@@ -97,24 +97,24 @@ func toSeconds(value float64) string {
absValue := math.Abs(value)
if absValue < 0.000001 {
return toFixedScaled(value*1e9, nil, " ns")
return toFixedScaled(value*1e9, " ns")
} else if absValue < 0.001 {
return toFixedScaled(value*1e6, nil, " µs")
return toFixedScaled(value*1e6, " µs")
} else if absValue < 1 {
return toFixedScaled(value*1e3, nil, " ms")
return toFixedScaled(value*1e3, " ms")
} else if absValue < 60 {
return toFixed(value, nil) + " s"
} else if absValue < 3600 {
return toFixedScaled(value/60, nil, " min")
return toFixedScaled(value/60, " min")
} else if absValue < 86400 { // 56000 s is better represented as 15.56 hour
return toFixedScaled(value/3600, nil, " hour")
return toFixedScaled(value/3600, " hour")
} else if absValue < 604800 {
return toFixedScaled(value/86400, nil, " day")
return toFixedScaled(value/86400, " day")
} else if absValue < 31536000 {
return toFixedScaled(value/604800, nil, " week")
return toFixedScaled(value/604800, " week")
}
return toFixedScaled(value/3.15569e7, nil, " year")
return toFixedScaled(value/3.15569e7, " year")
}
// toMinutes returns a easy to read string representation of the given value in minutes
@@ -124,13 +124,13 @@ func toMinutes(value float64) string {
if absValue < 60 {
return toFixed(value, nil) + " min"
} else if absValue < 1440 {
return toFixedScaled(value/60, nil, " hour")
return toFixedScaled(value/60, " hour")
} else if absValue < 10080 {
return toFixedScaled(value/1440, nil, " day")
return toFixedScaled(value/1440, " day")
} else if absValue < 604800 {
return toFixedScaled(value/10080, nil, " week")
return toFixedScaled(value/10080, " week")
} else {
return toFixedScaled(value/5.25948e5, nil, " year")
return toFixedScaled(value/5.25948e5, " year")
}
}
@@ -142,11 +142,11 @@ func toHours(value float64) string {
if absValue < 24 {
return toFixed(value, nil) + " hour"
} else if absValue < 168 {
return toFixedScaled(value/24, nil, " day")
return toFixedScaled(value/24, " day")
} else if absValue < 8760 {
return toFixedScaled(value/168, nil, " week")
return toFixedScaled(value/168, " week")
} else {
return toFixedScaled(value/8760, nil, " year")
return toFixedScaled(value/8760, " year")
}
}
@@ -157,9 +157,9 @@ func toDays(value float64) string {
if absValue < 7 {
return toFixed(value, nil) + " day"
} else if absValue < 365 {
return toFixedScaled(value/7, nil, " week")
return toFixedScaled(value/7, " week")
} else {
return toFixedScaled(value/365, nil, " year")
return toFixedScaled(value/365, " year")
}
}
@@ -170,6 +170,6 @@ func toWeeks(value float64) string {
if absValue < 52 {
return toFixed(value, nil) + " week"
} else {
return toFixedScaled(value/52, nil, " year")
return toFixedScaled(value/52, " year")
}
}

View File

@@ -1,4 +1,4 @@
package formatter
package units
import (
"testing"