mirror of
https://github.com/SigNoz/signoz.git
synced 2026-02-27 02:32:53 +00:00
Compare commits
11 Commits
feat/disab
...
SIGNOZ-870
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a83c477a00 | ||
|
|
d0748314e1 | ||
|
|
43933f3a33 | ||
|
|
d2e1a24b20 | ||
|
|
5bbe27946d | ||
|
|
e731c1c41b | ||
|
|
f407b3355b | ||
|
|
4f5b95e791 | ||
|
|
95d3928b32 | ||
|
|
8db22a5176 | ||
|
|
6ca329ba10 |
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || ''}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -337,11 +337,6 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled-row {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.k8s-list-table {
|
||||
.ant-table {
|
||||
.ant-table-thead > tr > th {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || ''}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || ''}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || ''}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || ''}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -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>
|
||||
|
||||
111
frontend/src/container/MetricsExplorer/Inspect/HoverPopover.tsx
Normal file
111
frontend/src/container/MetricsExplorer/Inspect/HoverPopover.tsx
Normal 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;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
@@ -333,7 +333,7 @@ function Summary(): JSX.Element {
|
||||
openInspectModal={openInspectModal}
|
||||
/>
|
||||
)}
|
||||
{isInspectModalOpen && (
|
||||
{isInspectModalOpen && selectedMetricName && (
|
||||
<InspectModal
|
||||
isOpen={isInspectModalOpen}
|
||||
onClose={closeInspectModal}
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
package converter
|
||||
package units
|
||||
|
||||
// boolConverter is a Converter implementation for bool
|
||||
type boolConverter struct{}
|
||||
@@ -1,4 +1,4 @@
|
||||
package converter
|
||||
package units
|
||||
|
||||
const (
|
||||
// base 10 (SI prefixes)
|
||||
@@ -1,4 +1,4 @@
|
||||
package converter
|
||||
package units
|
||||
|
||||
const (
|
||||
// base 10 (SI prefixes)
|
||||
@@ -1,4 +1,4 @@
|
||||
package converter
|
||||
package units
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package converter
|
||||
package units
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package converter
|
||||
package units
|
||||
|
||||
// percentConverter is a converter for percent unit
|
||||
type percentConverter struct{}
|
||||
@@ -1,4 +1,4 @@
|
||||
package converter
|
||||
package units
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package converter
|
||||
package units
|
||||
|
||||
// throughputConverter is an implementation of Converter that converts throughput
|
||||
type throughputConverter struct {
|
||||
@@ -1,4 +1,4 @@
|
||||
package converter
|
||||
package units
|
||||
|
||||
type Duration float64
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package converter
|
||||
package units
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -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
|
||||
@@ -1,4 +1,4 @@
|
||||
package formatter
|
||||
package units
|
||||
|
||||
import "fmt"
|
||||
|
||||
85
pkg/units/formatter_data.go
Normal file
85
pkg/units/formatter_data.go
Normal 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)
|
||||
}
|
||||
90
pkg/units/formatter_data_rate.go
Normal file
90
pkg/units/formatter_data_rate.go
Normal 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)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package formatter
|
||||
package units
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -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"))
|
||||
@@ -1,4 +1,4 @@
|
||||
package formatter
|
||||
package units
|
||||
|
||||
import "fmt"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package formatter
|
||||
package units
|
||||
|
||||
import "fmt"
|
||||
|
||||
@@ -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 {
|
||||
@@ -1,4 +1,4 @@
|
||||
package formatter
|
||||
package units
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -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)
|
||||
@@ -1,4 +1,4 @@
|
||||
package formatter
|
||||
package units
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package formatter
|
||||
package units
|
||||
|
||||
import (
|
||||
"testing"
|
||||
Reference in New Issue
Block a user