mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-07-29 01:02:01 +08:00
commit
e97609ce23
@ -137,7 +137,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.21.0
|
||||
image: signoz/query-service:0.22.0
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
@ -166,7 +166,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.21.0
|
||||
image: signoz/frontend:0.22.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@ -179,7 +179,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.79.1
|
||||
image: signoz/signoz-otel-collector:0.79.2
|
||||
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@ -208,7 +208,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:0.79.1
|
||||
image: signoz/signoz-otel-collector:0.79.2
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
@ -233,7 +233,7 @@ services:
|
||||
max-file: "3"
|
||||
|
||||
load-hotrod:
|
||||
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
|
||||
image: "signoz/locust:1.2.3"
|
||||
hostname: load-hotrod
|
||||
environment:
|
||||
ATTACKED_HOST: http://hotrod:8080
|
||||
|
@ -41,7 +41,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
otel-collector:
|
||||
container_name: otel-collector
|
||||
image: signoz/signoz-otel-collector:0.79.1
|
||||
image: signoz/signoz-otel-collector:0.79.2
|
||||
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||
# user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@ -67,7 +67,7 @@ services:
|
||||
|
||||
otel-collector-metrics:
|
||||
container_name: otel-collector-metrics
|
||||
image: signoz/signoz-otel-collector:0.79.1
|
||||
image: signoz/signoz-otel-collector:0.79.2
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
@ -93,7 +93,7 @@ services:
|
||||
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
|
||||
|
||||
load-hotrod:
|
||||
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
|
||||
image: "signoz/locust:1.2.3"
|
||||
container_name: load-hotrod
|
||||
hostname: load-hotrod
|
||||
environment:
|
||||
|
@ -153,7 +153,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.21.0}
|
||||
image: signoz/query-service:${DOCKER_TAG:-0.22.0}
|
||||
container_name: query-service
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
@ -181,7 +181,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.21.0}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.22.0}
|
||||
container_name: frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@ -193,7 +193,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.1}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.2}
|
||||
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
@ -219,7 +219,7 @@ services:
|
||||
<<: *clickhouse-depend
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.1}
|
||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.2}
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
@ -243,7 +243,7 @@ services:
|
||||
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
|
||||
|
||||
load-hotrod:
|
||||
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
|
||||
image: "signoz/locust:1.2.3"
|
||||
container_name: load-hotrod
|
||||
hostname: load-hotrod
|
||||
environment:
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
|
||||
type APIHandlerOptions struct {
|
||||
DataConnector interfaces.DataConnector
|
||||
SkipConfig *basemodel.SkipConfig
|
||||
AppDao dao.ModelDao
|
||||
RulesManager *rules.Manager
|
||||
FeatureFlags baseint.FeatureLookup
|
||||
@ -32,6 +33,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
|
||||
|
||||
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
||||
Reader: opts.DataConnector,
|
||||
SkipConfig: opts.SkipConfig,
|
||||
AppDao: opts.AppDao,
|
||||
RuleManager: opts.RulesManager,
|
||||
FeatureFlags: opts.FeatureFlags})
|
||||
|
@ -50,6 +50,7 @@ const AppDbEngine = "sqlite"
|
||||
|
||||
type ServerOptions struct {
|
||||
PromConfigPath string
|
||||
SkipTopLvlOpsPath string
|
||||
HTTPHostPort string
|
||||
PrivateHostPort string
|
||||
// alert specific params
|
||||
@ -119,7 +120,15 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
go qb.Start(readerReady)
|
||||
reader = qb
|
||||
} else {
|
||||
return nil, fmt.Errorf("Storage type: %s is not supported in query service", storage)
|
||||
return nil, fmt.Errorf("storage type: %s is not supported in query service", storage)
|
||||
}
|
||||
skipConfig := &basemodel.SkipConfig{}
|
||||
if serverOptions.SkipTopLvlOpsPath != "" {
|
||||
// read skip config
|
||||
skipConfig, err = basemodel.ReadSkipConfig(serverOptions.SkipTopLvlOpsPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
<-readerReady
|
||||
@ -160,6 +169,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
||||
|
||||
apiOpts := api.APIHandlerOptions{
|
||||
DataConnector: reader,
|
||||
SkipConfig: skipConfig,
|
||||
AppDao: modelDao,
|
||||
RulesManager: rm,
|
||||
FeatureFlags: lm,
|
||||
|
@ -74,7 +74,7 @@ func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
|
||||
}
|
||||
|
||||
func main() {
|
||||
var promConfigPath string
|
||||
var promConfigPath, skipTopLvlOpsPath string
|
||||
|
||||
// disables rule execution but allows change to the rule definition
|
||||
var disableRules bool
|
||||
@ -85,6 +85,7 @@ func main() {
|
||||
var enableQueryServiceLogOTLPExport bool
|
||||
|
||||
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
|
||||
flag.StringVar(&skipTopLvlOpsPath, "skip-top-level-ops", "", "(config file to skip top level operations)")
|
||||
flag.BoolVar(&disableRules, "rules.disable", false, "(disable rule evaluation)")
|
||||
flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)")
|
||||
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
|
||||
@ -100,6 +101,7 @@ func main() {
|
||||
serverOptions := &app.ServerOptions{
|
||||
HTTPHostPort: baseconst.HTTPHostPort,
|
||||
PromConfigPath: promConfigPath,
|
||||
SkipTopLvlOpsPath: skipTopLvlOpsPath,
|
||||
PrivateHostPort: baseconst.PrivateHostPort,
|
||||
DisableRules: disableRules,
|
||||
RuleRepoURL: ruleRepoURL,
|
||||
|
@ -181,6 +181,9 @@ function Graph({
|
||||
},
|
||||
},
|
||||
position: 'custom',
|
||||
itemSort(item1, item2) {
|
||||
return item2.parsed.y - item1.parsed.y;
|
||||
},
|
||||
},
|
||||
[dragSelectPluginId]: createDragSelectPluginOptions(
|
||||
!!onDragSelect,
|
||||
|
@ -1,14 +1,20 @@
|
||||
import { Table } from 'antd';
|
||||
import type { TableProps } from 'antd/es/table';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { SyntheticEvent, useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
SyntheticEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { ResizeCallbackData } from 'react-resizable';
|
||||
|
||||
import ResizableHeader from './ResizableHeader';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function ResizeTable({ columns, ...restprops }: TableProps<any>): JSX.Element {
|
||||
const [columnsData, setColumns] = useState<ColumnsType>(columns || []);
|
||||
const [columnsData, setColumns] = useState<ColumnsType>([]);
|
||||
|
||||
const handleResize = useCallback(
|
||||
(index: number) => (
|
||||
@ -37,6 +43,12 @@ function ResizeTable({ columns, ...restprops }: TableProps<any>): JSX.Element {
|
||||
[columnsData, handleResize],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (columns) {
|
||||
setColumns(columns);
|
||||
}
|
||||
}, [columns]);
|
||||
|
||||
return (
|
||||
<Table
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
|
@ -12,4 +12,8 @@ export enum QueryParams {
|
||||
aggregationOption = 'aggregationOption',
|
||||
entity = 'entity',
|
||||
resourceAttributes = 'resourceAttribute',
|
||||
graphType = 'graphType',
|
||||
widgetId = 'widgetId',
|
||||
order = 'order',
|
||||
q = 'q',
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ const initialQueryBuilderFormValues: IBuilderQuery = {
|
||||
}),
|
||||
disabled: false,
|
||||
having: [],
|
||||
stepInterval: 30,
|
||||
stepInterval: 60,
|
||||
limit: null,
|
||||
orderBy: [],
|
||||
groupBy: [],
|
||||
@ -232,6 +232,7 @@ export const PANEL_TYPES: Record<PanelTypeKeys, GRAPH_TYPES> = {
|
||||
VALUE: 'value',
|
||||
TABLE: 'table',
|
||||
LIST: 'list',
|
||||
TRACE: 'trace',
|
||||
EMPTY_WIDGET: 'EMPTY_WIDGET',
|
||||
};
|
||||
|
||||
|
@ -1,33 +1,30 @@
|
||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { Button, Select } from 'antd';
|
||||
import { DEFAULT_PER_PAGE_OPTIONS, Pagination } from 'hooks/queryPagination';
|
||||
import { memo, useMemo } from 'react';
|
||||
|
||||
import { defaultSelectStyle, ITEMS_PER_PAGE_OPTIONS } from './config';
|
||||
import { defaultSelectStyle } from './config';
|
||||
import { Container } from './styles';
|
||||
|
||||
interface ControlsProps {
|
||||
count: number;
|
||||
countPerPage: number;
|
||||
isLoading: boolean;
|
||||
handleNavigatePrevious: () => void;
|
||||
handleNavigateNext: () => void;
|
||||
handleCountItemsPerPageChange: (e: number) => void;
|
||||
}
|
||||
|
||||
function Controls(props: ControlsProps): JSX.Element | null {
|
||||
const {
|
||||
count,
|
||||
function Controls({
|
||||
offset = 0,
|
||||
perPageOptions = DEFAULT_PER_PAGE_OPTIONS,
|
||||
isLoading,
|
||||
totalCount,
|
||||
countPerPage,
|
||||
handleNavigatePrevious,
|
||||
handleNavigateNext,
|
||||
handleCountItemsPerPageChange,
|
||||
} = props;
|
||||
|
||||
}: ControlsProps): JSX.Element | null {
|
||||
const isNextAndPreviousDisabled = useMemo(
|
||||
() => isLoading || countPerPage === 0 || count === 0 || count < countPerPage,
|
||||
[isLoading, countPerPage, count],
|
||||
() => isLoading || countPerPage < 0 || totalCount === 0,
|
||||
[isLoading, countPerPage, totalCount],
|
||||
);
|
||||
const isPreviousDisabled = useMemo(() => offset <= 0, [offset]);
|
||||
const isNextDisabled = useMemo(() => totalCount < countPerPage, [
|
||||
countPerPage,
|
||||
totalCount,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@ -35,7 +32,7 @@ function Controls(props: ControlsProps): JSX.Element | null {
|
||||
loading={isLoading}
|
||||
size="small"
|
||||
type="link"
|
||||
disabled={isNextAndPreviousDisabled}
|
||||
disabled={isPreviousDisabled || isNextAndPreviousDisabled}
|
||||
onClick={handleNavigatePrevious}
|
||||
>
|
||||
<LeftOutlined /> Previous
|
||||
@ -44,18 +41,18 @@ function Controls(props: ControlsProps): JSX.Element | null {
|
||||
loading={isLoading}
|
||||
size="small"
|
||||
type="link"
|
||||
disabled={isNextAndPreviousDisabled}
|
||||
disabled={isNextDisabled || isNextAndPreviousDisabled}
|
||||
onClick={handleNavigateNext}
|
||||
>
|
||||
Next <RightOutlined />
|
||||
</Button>
|
||||
<Select
|
||||
<Select<Pagination['limit']>
|
||||
style={defaultSelectStyle}
|
||||
loading={isLoading}
|
||||
value={countPerPage}
|
||||
onChange={handleCountItemsPerPageChange}
|
||||
>
|
||||
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
|
||||
{perPageOptions.map((count) => (
|
||||
<Select.Option
|
||||
key={count}
|
||||
value={count}
|
||||
@ -66,4 +63,20 @@ function Controls(props: ControlsProps): JSX.Element | null {
|
||||
);
|
||||
}
|
||||
|
||||
Controls.defaultProps = {
|
||||
offset: 0,
|
||||
perPageOptions: DEFAULT_PER_PAGE_OPTIONS,
|
||||
};
|
||||
|
||||
export interface ControlsProps {
|
||||
offset?: Pagination['offset'];
|
||||
perPageOptions?: number[];
|
||||
totalCount: number;
|
||||
countPerPage: Pagination['limit'];
|
||||
isLoading: boolean;
|
||||
handleNavigatePrevious: () => void;
|
||||
handleNavigateNext: () => void;
|
||||
handleCountItemsPerPageChange: (value: Pagination['limit']) => void;
|
||||
}
|
||||
|
||||
export default memo(Controls);
|
||||
|
@ -0,0 +1,27 @@
|
||||
import { TFunction } from 'i18next';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { OptionType } from './types';
|
||||
|
||||
export const getOptionList = (t: TFunction): OptionType[] => [
|
||||
{
|
||||
title: t('metric_based_alert'),
|
||||
selection: AlertTypes.METRICS_BASED_ALERT,
|
||||
description: t('metric_based_alert_desc'),
|
||||
},
|
||||
{
|
||||
title: t('log_based_alert'),
|
||||
selection: AlertTypes.LOGS_BASED_ALERT,
|
||||
description: t('log_based_alert_desc'),
|
||||
},
|
||||
{
|
||||
title: t('traces_based_alert'),
|
||||
selection: AlertTypes.TRACES_BASED_ALERT,
|
||||
description: t('traces_based_alert_desc'),
|
||||
},
|
||||
{
|
||||
title: t('exceptions_based_alert'),
|
||||
selection: AlertTypes.EXCEPTIONS_BASED_ALERT,
|
||||
description: t('exceptions_based_alert_desc'),
|
||||
},
|
||||
];
|
@ -1,61 +1,40 @@
|
||||
import { Row } from 'antd';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
import { getOptionList } from './config';
|
||||
import { AlertTypeCard, SelectTypeContainer } from './styles';
|
||||
|
||||
interface OptionType {
|
||||
title: string;
|
||||
selection: AlertTypes;
|
||||
description: string;
|
||||
}
|
||||
import { OptionType } from './types';
|
||||
|
||||
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
const { t } = useTranslation(['alerts']);
|
||||
|
||||
const renderOptions = (): JSX.Element => {
|
||||
const optionList: OptionType[] = [
|
||||
{
|
||||
title: t('metric_based_alert'),
|
||||
selection: AlertTypes.METRICS_BASED_ALERT,
|
||||
description: t('metric_based_alert_desc'),
|
||||
},
|
||||
{
|
||||
title: t('log_based_alert'),
|
||||
selection: AlertTypes.LOGS_BASED_ALERT,
|
||||
description: t('log_based_alert_desc'),
|
||||
},
|
||||
{
|
||||
title: t('traces_based_alert'),
|
||||
selection: AlertTypes.TRACES_BASED_ALERT,
|
||||
description: t('traces_based_alert_desc'),
|
||||
},
|
||||
{
|
||||
title: t('exceptions_based_alert'),
|
||||
selection: AlertTypes.EXCEPTIONS_BASED_ALERT,
|
||||
description: t('exceptions_based_alert_desc'),
|
||||
},
|
||||
];
|
||||
return (
|
||||
const optionList = getOptionList(t);
|
||||
|
||||
const renderOptions = useMemo(
|
||||
() => (
|
||||
<>
|
||||
{optionList.map((o: OptionType) => (
|
||||
{optionList.map((option: OptionType) => (
|
||||
<AlertTypeCard
|
||||
key={o.selection}
|
||||
title={o.title}
|
||||
key={option.selection}
|
||||
title={option.title}
|
||||
onClick={(): void => {
|
||||
onSelect(o.selection);
|
||||
onSelect(option.selection);
|
||||
}}
|
||||
>
|
||||
{o.description}
|
||||
{option.description}
|
||||
</AlertTypeCard>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
[onSelect, optionList],
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectTypeContainer>
|
||||
<h3> {t('choose_alert_type')} </h3>
|
||||
<Row>{renderOptions()}</Row>
|
||||
<Row>{renderOptions}</Row>
|
||||
</SelectTypeContainer>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
|
||||
export interface OptionType {
|
||||
title: string;
|
||||
selection: AlertTypes;
|
||||
description: string;
|
||||
}
|
8
frontend/src/container/CreateAlertRule/config.ts
Normal file
8
frontend/src/container/CreateAlertRule/config.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const ALERT_TYPE_VS_SOURCE_MAPPING = {
|
||||
[DataSource.LOGS]: AlertTypes.LOGS_BASED_ALERT,
|
||||
[DataSource.METRICS]: AlertTypes.METRICS_BASED_ALERT,
|
||||
[DataSource.TRACES]: AlertTypes.TRACES_BASED_ALERT,
|
||||
};
|
@ -38,7 +38,7 @@ export const alertDefaults: AlertDef = {
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
queryType: EQueryType.CLICKHOUSE,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
op: defaultCompareOp,
|
||||
@ -67,7 +67,7 @@ export const logAlertDefaults: AlertDef = {
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
queryType: EQueryType.CLICKHOUSE,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
op: defaultCompareOp,
|
||||
@ -97,7 +97,7 @@ export const traceAlertDefaults: AlertDef = {
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
queryType: EQueryType.CLICKHOUSE,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
op: defaultCompareOp,
|
||||
@ -127,7 +127,7 @@ export const exceptionAlertDefaults: AlertDef = {
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
queryType: EQueryType.CLICKHOUSE,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
op: defaultCompareOp,
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { Form, Row } from 'antd';
|
||||
import FormAlertRules from 'container/FormAlertRules';
|
||||
import { useState } from 'react';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { AlertDef } from 'types/api/alerts/def';
|
||||
|
||||
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
|
||||
import {
|
||||
alertDefaults,
|
||||
exceptionAlertDefaults,
|
||||
@ -17,6 +19,9 @@ function CreateRules(): JSX.Element {
|
||||
const [alertType, setAlertType] = useState<AlertTypes>(
|
||||
AlertTypes.METRICS_BASED_ALERT,
|
||||
);
|
||||
|
||||
const compositeQuery = useGetCompositeQueryParam();
|
||||
|
||||
const [formInstance] = Form.useForm();
|
||||
|
||||
const onSelectType = (typ: AlertTypes): void => {
|
||||
@ -36,6 +41,19 @@ function CreateRules(): JSX.Element {
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!compositeQuery) {
|
||||
return;
|
||||
}
|
||||
const dataSource = compositeQuery?.builder?.queryData[0]?.dataSource;
|
||||
|
||||
const alertType = ALERT_TYPE_VS_SOURCE_MAPPING[dataSource];
|
||||
|
||||
if (alertType) {
|
||||
onSelectType(alertType);
|
||||
}
|
||||
}, [compositeQuery]);
|
||||
|
||||
if (!initValues) {
|
||||
return (
|
||||
<Row wrap={false}>
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { Button, Typography } from 'antd';
|
||||
import createDashboard from 'api/dashboard/create';
|
||||
import getAll from 'api/dashboard/getAll';
|
||||
import axios from 'axios';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { useMutation } from 'react-query';
|
||||
|
||||
import { ExportPanelProps } from '.';
|
||||
import {
|
||||
@ -18,7 +17,7 @@ import {
|
||||
} from './styles';
|
||||
import { getSelectOptions } from './utils';
|
||||
|
||||
function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
|
||||
function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
|
||||
@ -26,16 +25,18 @@ function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
|
||||
null,
|
||||
);
|
||||
|
||||
const { data, isLoading, refetch } = useQuery({
|
||||
queryFn: getAll,
|
||||
queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS,
|
||||
});
|
||||
const {
|
||||
data,
|
||||
isLoading: isAllDashboardsLoading,
|
||||
refetch,
|
||||
} = useGetAllDashboard();
|
||||
|
||||
const {
|
||||
mutate: createNewDashboard,
|
||||
isLoading: createDashboardLoading,
|
||||
} = useMutation(createDashboard, {
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
onExport(data?.payload || null);
|
||||
refetch();
|
||||
},
|
||||
onError: (error) => {
|
||||
@ -73,6 +74,14 @@ function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
|
||||
});
|
||||
}, [t, createNewDashboard]);
|
||||
|
||||
const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading;
|
||||
|
||||
const isDisabled =
|
||||
isAllDashboardsLoading ||
|
||||
!options?.length ||
|
||||
!selectedDashboardId ||
|
||||
isLoading;
|
||||
|
||||
return (
|
||||
<Wrapper direction="vertical">
|
||||
<Title>Export Panel</Title>
|
||||
@ -81,14 +90,15 @@ function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
|
||||
<DashboardSelect
|
||||
placeholder="Select Dashboard"
|
||||
options={options}
|
||||
loading={isLoading || createDashboardLoading}
|
||||
disabled={isLoading || createDashboardLoading}
|
||||
loading={isDashboardLoading}
|
||||
disabled={isDashboardLoading}
|
||||
value={selectedDashboardId}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
disabled={isLoading || !options?.length || !selectedDashboardId}
|
||||
loading={isLoading}
|
||||
disabled={isDisabled}
|
||||
onClick={handleExportClick}
|
||||
>
|
||||
Export
|
||||
|
@ -1,24 +1,44 @@
|
||||
import { Button, Dropdown, MenuProps, Modal } from 'antd';
|
||||
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { MENU_KEY, MENU_LABEL } from './config';
|
||||
import ExportPanelContainer from './ExportPanel';
|
||||
|
||||
function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
|
||||
function ExportPanel({
|
||||
isLoading,
|
||||
onExport,
|
||||
query,
|
||||
}: ExportPanelProps): JSX.Element {
|
||||
const [isExport, setIsExport] = useState<boolean>(false);
|
||||
|
||||
const onModalToggle = useCallback((value: boolean) => {
|
||||
setIsExport(value);
|
||||
}, []);
|
||||
|
||||
const onCreateAlertsHandler = useCallback(() => {
|
||||
history.push(
|
||||
`${ROUTES.ALERTS_NEW}?${COMPOSITE_QUERY}=${encodeURIComponent(
|
||||
JSON.stringify(query),
|
||||
)}`,
|
||||
);
|
||||
}, [query]);
|
||||
|
||||
const onMenuClickHandler: MenuProps['onClick'] = useCallback(
|
||||
(e: OnClickProps) => {
|
||||
if (e.key === MENU_KEY.EXPORT) {
|
||||
onModalToggle(true);
|
||||
}
|
||||
|
||||
if (e.key === MENU_KEY.CREATE_ALERTS) {
|
||||
onCreateAlertsHandler();
|
||||
}
|
||||
},
|
||||
[onModalToggle],
|
||||
[onModalToggle, onCreateAlertsHandler],
|
||||
);
|
||||
|
||||
const menu: MenuProps = useMemo(
|
||||
@ -48,23 +68,34 @@ function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
|
||||
<Button>Actions</Button>
|
||||
</Dropdown>
|
||||
<Modal
|
||||
footer={null}
|
||||
onOk={onCancel(false)}
|
||||
onCancel={onCancel(false)}
|
||||
open={isExport}
|
||||
centered
|
||||
>
|
||||
<ExportPanelContainer onExport={onExport} />
|
||||
<ExportPanelContainer
|
||||
query={query}
|
||||
isLoading={isLoading}
|
||||
onExport={onExport}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ExportPanel.defaultProps = {
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
interface OnClickProps {
|
||||
key: string;
|
||||
}
|
||||
|
||||
export interface ExportPanelProps {
|
||||
isLoading?: boolean;
|
||||
onExport: (dashboard: Dashboard | null) => void;
|
||||
query: Query | null;
|
||||
}
|
||||
|
||||
export default ExportPanel;
|
||||
|
@ -8,6 +8,7 @@ import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
|
||||
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
@ -16,6 +17,8 @@ import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQu
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import {
|
||||
AlertDef,
|
||||
@ -24,6 +27,7 @@ import {
|
||||
} from 'types/api/alerts/def';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import BasicInfo from './BasicInfo';
|
||||
import ChartPreview from './ChartPreview';
|
||||
@ -48,6 +52,10 @@ function FormAlertRules({
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const {
|
||||
currentQuery,
|
||||
stagedQuery,
|
||||
@ -70,16 +78,12 @@ function FormAlertRules({
|
||||
|
||||
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
|
||||
|
||||
useShareBuilderUrl({ defaultValue: sq });
|
||||
useShareBuilderUrl(sq);
|
||||
|
||||
useEffect(() => {
|
||||
setAlertDef(initialValue);
|
||||
}, [initialValue]);
|
||||
|
||||
const onRunQuery = (): void => {
|
||||
handleRunQuery();
|
||||
};
|
||||
|
||||
const onCancelHandler = useCallback(() => {
|
||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||
}, []);
|
||||
@ -99,7 +103,7 @@ function FormAlertRules({
|
||||
}
|
||||
const query: Query = { ...currentQuery, queryType: val };
|
||||
|
||||
redirectWithQueryBuilderData(query);
|
||||
redirectWithQueryBuilderData(updateStepInterval(query, maxTime, minTime));
|
||||
};
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
@ -402,7 +406,7 @@ function FormAlertRules({
|
||||
queryCategory={currentQuery.queryType}
|
||||
setQueryCategory={onQueryCategoryChange}
|
||||
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
|
||||
runQuery={onRunQuery}
|
||||
runQuery={handleRunQuery}
|
||||
/>
|
||||
|
||||
<RuleOptions
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
timePreferance,
|
||||
} from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
@ -48,11 +49,13 @@ function FullView({
|
||||
[selectedTime, globalSelectedTime, widget],
|
||||
);
|
||||
|
||||
const updatedQuery = useStepInterval(widget?.query);
|
||||
|
||||
const response = useGetQueryRange(
|
||||
{
|
||||
selectedTime: selectedTime.enum,
|
||||
graphType: widget.panelTypes,
|
||||
query: widget.query,
|
||||
query: updatedQuery,
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
variables: getDashboardVariables(),
|
||||
},
|
||||
@ -84,10 +87,8 @@ function FullView({
|
||||
{fullViewOptions && (
|
||||
<TimeContainer>
|
||||
<TimePreference
|
||||
{...{
|
||||
selectedTime,
|
||||
setSelectedTime,
|
||||
}}
|
||||
selectedTime={selectedTime}
|
||||
setSelectedTime={setSelectedTime}
|
||||
/>
|
||||
<Button
|
||||
onClick={(): void => {
|
||||
|
@ -4,6 +4,7 @@ import Spinner from 'components/Spinner';
|
||||
import GridGraphComponent from 'container/GridGraphComponent';
|
||||
import { UpdateDashboard } from 'container/GridGraphLayout/utils';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import usePreviousValue from 'hooks/usePreviousValue';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
@ -80,11 +81,13 @@ function GridCardGraph({
|
||||
const selectedData = selectedDashboard?.data;
|
||||
const { variables } = selectedData;
|
||||
|
||||
const updatedQuery = useStepInterval(widget?.query);
|
||||
|
||||
const queryResponse = useGetQueryRange(
|
||||
{
|
||||
selectedTime: widget?.timePreferance,
|
||||
graphType: widget?.panelTypes,
|
||||
query: widget?.query,
|
||||
query: updatedQuery,
|
||||
globalSelectedInterval,
|
||||
variables: getDashboardVariables(),
|
||||
},
|
||||
|
@ -64,7 +64,7 @@ function WidgetHeader({
|
||||
history.push(
|
||||
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${
|
||||
widget.panelTypes
|
||||
}&${COMPOSITE_QUERY}=${JSON.stringify(widget.query)}`,
|
||||
}&${COMPOSITE_QUERY}=${encodeURIComponent(JSON.stringify(widget.query))}`,
|
||||
);
|
||||
}, [widget.id, widget.panelTypes, widget.query]);
|
||||
|
||||
|
@ -77,8 +77,8 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
||||
history.push(
|
||||
`${
|
||||
ROUTES.EDIT_ALERTS
|
||||
}?ruleId=${record.id.toString()}&${COMPOSITE_QUERY}=${JSON.stringify(
|
||||
compositeQuery,
|
||||
}?ruleId=${record.id.toString()}&${COMPOSITE_QUERY}=${encodeURIComponent(
|
||||
JSON.stringify(compositeQuery),
|
||||
)}`,
|
||||
);
|
||||
})
|
||||
|
@ -75,8 +75,8 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
errorMessage: '',
|
||||
});
|
||||
|
||||
const columns: TableColumnProps<Data>[] = useMemo(
|
||||
() => [
|
||||
const columns = useMemo(() => {
|
||||
const tableColumns: TableColumnProps<Data>[] = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
@ -118,20 +118,20 @@ function ListOfAllDashboard(): JSX.Element {
|
||||
},
|
||||
render: DateComponent,
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
];
|
||||
|
||||
if (action) {
|
||||
columns.push({
|
||||
tableColumns.push({
|
||||
title: 'Action',
|
||||
dataIndex: '',
|
||||
key: 'x',
|
||||
width: 40,
|
||||
render: DeleteButton,
|
||||
});
|
||||
}
|
||||
|
||||
return tableColumns;
|
||||
}, [action]);
|
||||
|
||||
const data: Data[] = (filteredDashboards || dashboards).map((e) => ({
|
||||
createdBy: e.created_at,
|
||||
description: e.data.description || '',
|
||||
|
@ -5,7 +5,9 @@ import Controls from 'container/Controls';
|
||||
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
|
||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||
import dayjs from 'dayjs';
|
||||
import { Pagination } from 'hooks/queryPagination';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { OrderPreferenceItems } from 'pages/Logs/config';
|
||||
import * as Papa from 'papaparse';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
@ -30,6 +32,7 @@ function LogControls(): JSX.Element | null {
|
||||
isLoading: isLogsLoading,
|
||||
isLoadingAggregate,
|
||||
logs,
|
||||
order,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
@ -37,7 +40,7 @@ function LogControls(): JSX.Element | null {
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
const handleLogLinesPerPageChange = (e: number): void => {
|
||||
const handleLogLinesPerPageChange = (e: Pagination['limit']): void => {
|
||||
dispatch({
|
||||
type: SET_LOG_LINES_PER_PAGE,
|
||||
payload: {
|
||||
@ -159,6 +162,7 @@ function LogControls(): JSX.Element | null {
|
||||
loading={isLoading}
|
||||
size="small"
|
||||
type="link"
|
||||
disabled={order === OrderPreferenceItems.ASC}
|
||||
onClick={handleGoToLatest}
|
||||
>
|
||||
<FastBackwardOutlined /> Go to latest
|
||||
@ -166,7 +170,7 @@ function LogControls(): JSX.Element | null {
|
||||
<Divider type="vertical" />
|
||||
<Controls
|
||||
isLoading={isLoading}
|
||||
count={logs.length}
|
||||
totalCount={logs.length}
|
||||
countPerPage={logLinesPerPage}
|
||||
handleNavigatePrevious={handleNavigatePrevious}
|
||||
handleNavigateNext={handleNavigateNext}
|
||||
|
@ -2,6 +2,7 @@ import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Col, Popover } from 'antd';
|
||||
import getStep from 'lib/getStep';
|
||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||
import { getIdConditions } from 'pages/Logs/utils';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
@ -45,6 +46,7 @@ function ActionItem({
|
||||
idStart,
|
||||
liveTail,
|
||||
idEnd,
|
||||
order,
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
|
||||
@ -72,11 +74,10 @@ function ActionItem({
|
||||
q: updatedQueryString,
|
||||
limit: logLinesPerPage,
|
||||
orderBy: 'timestamp',
|
||||
order: 'desc',
|
||||
order,
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
...getIdConditions(idStart, idEnd, order),
|
||||
});
|
||||
getLogsAggregate({
|
||||
timestampStart: minTime,
|
||||
|
@ -1 +0,0 @@
|
||||
export { LogsExplorerChart } from './LogsExplorerChart';
|
@ -2,41 +2,33 @@ import Graph from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getExplorerChartData } from 'lib/explorer/getExplorerChartData';
|
||||
import { useMemo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { CardStyled } from './LogsExplorerChart.styled';
|
||||
|
||||
export function LogsExplorerChart(): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
function LogsExplorerChart(): JSX.Element {
|
||||
const { stagedQuery, panelType, isEnabledQuery } = useQueryBuilder();
|
||||
|
||||
const { selectedTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const panelTypeParam = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
const { data, isFetching } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap.metrics,
|
||||
graphType: panelTypeParam,
|
||||
graphType: panelType || PANEL_TYPES.LIST,
|
||||
globalSelectedInterval: selectedTime,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
selectedTime,
|
||||
stagedQuery,
|
||||
panelTypeParam,
|
||||
],
|
||||
enabled: !!stagedQuery,
|
||||
queryKey: [REACT_QUERY_KEY.GET_QUERY_RANGE, selectedTime, stagedQuery],
|
||||
enabled: isEnabledQuery,
|
||||
},
|
||||
);
|
||||
|
||||
@ -64,3 +56,5 @@ export function LogsExplorerChart(): JSX.Element {
|
||||
</CardStyled>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LogsExplorerChart);
|
@ -0,0 +1,3 @@
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
export type LogsExplorerListProps = { data: QueryDataV3[]; isLoading: boolean };
|
117
frontend/src/container/LogsExplorerList/index.tsx
Normal file
117
frontend/src/container/LogsExplorerList/index.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { Card, Typography } from 'antd';
|
||||
// components
|
||||
import ListLogView from 'components/Logs/ListLogView';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import LogsTableView from 'components/Logs/TableView';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { LogViewMode } from 'container/LogsTable';
|
||||
import { Container, Heading } from 'container/LogsTable/styles';
|
||||
import { contentStyle } from 'container/Trace/Search/config';
|
||||
import useFontFaceObserver from 'hooks/useFontObserver';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
// interfaces
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
|
||||
|
||||
function LogsExplorerList({
|
||||
data,
|
||||
isLoading,
|
||||
}: LogsExplorerListProps): JSX.Element {
|
||||
const [viewMode] = useState<LogViewMode>('raw');
|
||||
const [linesPerRow] = useState<number>(20);
|
||||
|
||||
const logs: ILog[] = useMemo(() => {
|
||||
if (data.length > 0 && data[0].list) {
|
||||
const logs: ILog[] = data[0].list.map((item) => ({
|
||||
timestamp: +item.timestamp,
|
||||
...item.data,
|
||||
}));
|
||||
|
||||
return logs;
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [data]);
|
||||
|
||||
useFontFaceObserver(
|
||||
[
|
||||
{
|
||||
family: 'Fira Code',
|
||||
weight: '300',
|
||||
},
|
||||
],
|
||||
viewMode === 'raw',
|
||||
{
|
||||
timeout: 5000,
|
||||
},
|
||||
);
|
||||
// TODO: implement here linesPerRow, mode like in useSelectedLogView
|
||||
|
||||
const getItemContent = useCallback(
|
||||
(index: number): JSX.Element => {
|
||||
const log = logs[index];
|
||||
|
||||
if (viewMode === 'raw') {
|
||||
return (
|
||||
<RawLogView
|
||||
key={log.id}
|
||||
data={log}
|
||||
linesPerRow={linesPerRow}
|
||||
// TODO: write new onClickExpanded logic
|
||||
onClickExpand={(): void => {}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ListLogView key={log.id} logData={log} />;
|
||||
},
|
||||
[logs, linesPerRow, viewMode],
|
||||
);
|
||||
|
||||
const renderContent = useMemo(() => {
|
||||
if (viewMode === 'table') {
|
||||
return (
|
||||
<LogsTableView
|
||||
logs={logs}
|
||||
// TODO: write new selected logic
|
||||
fields={[]}
|
||||
linesPerRow={linesPerRow}
|
||||
// TODO: write new onClickExpanded logic
|
||||
onClickExpand={(): void => {}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card bodyStyle={contentStyle}>
|
||||
<Virtuoso
|
||||
useWindowScroll
|
||||
totalCount={logs.length}
|
||||
itemContent={getItemContent}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}, [getItemContent, linesPerRow, logs, viewMode]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner height={20} tip="Getting Logs" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{viewMode !== 'table' && (
|
||||
<Heading>
|
||||
<Typography.Text>Event</Typography.Text>
|
||||
</Heading>
|
||||
)}
|
||||
|
||||
{logs.length === 0 && <Typography>No logs lines found</Typography>}
|
||||
|
||||
{renderContent}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LogsExplorerList);
|
@ -0,0 +1,6 @@
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
export type LogsExplorerTableProps = {
|
||||
data: QueryDataV3[];
|
||||
isLoading: boolean;
|
||||
};
|
23
frontend/src/container/LogsExplorerTable/index.tsx
Normal file
23
frontend/src/container/LogsExplorerTable/index.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { QueryTable } from 'container/QueryTable';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { LogsExplorerTableProps } from './LogsExplorerTable.interfaces';
|
||||
|
||||
function LogsExplorerTable({
|
||||
isLoading,
|
||||
data,
|
||||
}: LogsExplorerTableProps): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
return (
|
||||
<QueryTable
|
||||
query={stagedQuery || initialQueriesMap.metrics}
|
||||
queryTableData={data}
|
||||
loading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LogsExplorerTable);
|
@ -1,75 +0,0 @@
|
||||
import { TabsProps } from 'antd';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
import { TabsStyled } from './LogsExplorerViews.styled';
|
||||
|
||||
export function LogsExplorerViews(): JSX.Element {
|
||||
const location = useLocation();
|
||||
const urlQuery = useUrlQuery();
|
||||
const history = useHistory();
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
const panelTypeParams = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
const isMultipleQueries = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData.length > 1 ||
|
||||
currentQuery.builder.queryFormulas.length > 0,
|
||||
[currentQuery],
|
||||
);
|
||||
|
||||
const tabsItems: TabsProps['items'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'List View',
|
||||
key: PANEL_TYPES.LIST,
|
||||
disabled: isMultipleQueries,
|
||||
},
|
||||
{ label: 'TimeSeries', key: PANEL_TYPES.TIME_SERIES },
|
||||
{ label: 'Table', key: PANEL_TYPES.TABLE },
|
||||
],
|
||||
[isMultipleQueries],
|
||||
);
|
||||
|
||||
const handleChangeView = useCallback(
|
||||
(panelType: string) => {
|
||||
urlQuery.set(PANEL_TYPES_QUERY, JSON.stringify(panelType) as GRAPH_TYPES);
|
||||
const path = `${location.pathname}?${urlQuery}`;
|
||||
|
||||
history.push(path);
|
||||
},
|
||||
[history, location, urlQuery],
|
||||
);
|
||||
|
||||
const currentTabKey = useMemo(
|
||||
() =>
|
||||
Object.values(PANEL_TYPES).includes(panelTypeParams)
|
||||
? panelTypeParams
|
||||
: PANEL_TYPES.LIST,
|
||||
[panelTypeParams],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (panelTypeParams === 'list' && isMultipleQueries) {
|
||||
handleChangeView(PANEL_TYPES.TIME_SERIES);
|
||||
}
|
||||
}, [panelTypeParams, isMultipleQueries, handleChangeView]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TabsStyled
|
||||
items={tabsItems}
|
||||
defaultActiveKey={currentTabKey}
|
||||
activeKey={currentTabKey}
|
||||
onChange={handleChangeView}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1 +0,0 @@
|
||||
export { LogsExplorerViews } from './LogsExplorerViews';
|
134
frontend/src/container/LogsExplorerViews/index.tsx
Normal file
134
frontend/src/container/LogsExplorerViews/index.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import { TabsProps } from 'antd';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import LogsExplorerList from 'container/LogsExplorerList';
|
||||
import LogsExplorerTable from 'container/LogsExplorerTable';
|
||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { TabsStyled } from './LogsExplorerViews.styled';
|
||||
|
||||
function LogsExplorerViews(): JSX.Element {
|
||||
const {
|
||||
currentQuery,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
isEnabledQuery,
|
||||
updateAllQueriesOperators,
|
||||
redirectWithQueryBuilderData,
|
||||
} = useQueryBuilder();
|
||||
|
||||
const { selectedTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const { data, isFetching, isError } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap.metrics,
|
||||
graphType: panelType || PANEL_TYPES.LIST,
|
||||
globalSelectedInterval: selectedTime,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
params: {
|
||||
dataSource: DataSource.LOGS,
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: [REACT_QUERY_KEY.GET_QUERY_RANGE, selectedTime, stagedQuery],
|
||||
enabled: isEnabledQuery,
|
||||
},
|
||||
);
|
||||
|
||||
const isMultipleQueries = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData.length > 1 ||
|
||||
currentQuery.builder.queryFormulas.length > 0,
|
||||
[currentQuery],
|
||||
);
|
||||
|
||||
const isGroupByExist = useMemo(() => {
|
||||
const groupByCount: number = currentQuery.builder.queryData.reduce<number>(
|
||||
(acc, query) => acc + query.groupBy.length,
|
||||
0,
|
||||
);
|
||||
|
||||
return groupByCount > 0;
|
||||
}, [currentQuery]);
|
||||
|
||||
const currentData = useMemo(
|
||||
() => data?.payload.data.newResult.data.result || [],
|
||||
[data],
|
||||
);
|
||||
|
||||
const tabsItems: TabsProps['items'] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'List View',
|
||||
key: PANEL_TYPES.LIST,
|
||||
disabled: isMultipleQueries || isGroupByExist,
|
||||
children: <LogsExplorerList data={currentData} isLoading={isFetching} />,
|
||||
},
|
||||
{
|
||||
label: 'TimeSeries',
|
||||
key: PANEL_TYPES.TIME_SERIES,
|
||||
children: (
|
||||
<TimeSeriesView isLoading={isFetching} data={data} isError={isError} />
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Table',
|
||||
key: PANEL_TYPES.TABLE,
|
||||
children: <LogsExplorerTable data={currentData} isLoading={isFetching} />,
|
||||
},
|
||||
],
|
||||
[isMultipleQueries, isGroupByExist, currentData, isFetching, data, isError],
|
||||
);
|
||||
|
||||
const handleChangeView = useCallback(
|
||||
(newPanelType: string) => {
|
||||
if (newPanelType === panelType) return;
|
||||
|
||||
const query = updateAllQueriesOperators(
|
||||
currentQuery,
|
||||
newPanelType as GRAPH_TYPES,
|
||||
DataSource.LOGS,
|
||||
);
|
||||
|
||||
redirectWithQueryBuilderData(query, { [PANEL_TYPES_QUERY]: newPanelType });
|
||||
},
|
||||
[
|
||||
currentQuery,
|
||||
panelType,
|
||||
updateAllQueriesOperators,
|
||||
redirectWithQueryBuilderData,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const shouldChangeView = isMultipleQueries || isGroupByExist;
|
||||
|
||||
if (panelType === 'list' && shouldChangeView) {
|
||||
handleChangeView(PANEL_TYPES.TIME_SERIES);
|
||||
}
|
||||
}, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TabsStyled
|
||||
items={tabsItems}
|
||||
defaultActiveKey={panelType || PANEL_TYPES.LIST}
|
||||
activeKey={panelType || PANEL_TYPES.LIST}
|
||||
onChange={handleChangeView}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(LogsExplorerViews);
|
@ -2,6 +2,7 @@ import { Input, InputRef, Popover } from 'antd';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import getStep from 'lib/getStep';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
import { getIdConditions } from 'pages/Logs/utils';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
@ -33,7 +34,7 @@ function SearchFilter({
|
||||
const [searchText, setSearchText] = useState(queryString);
|
||||
const [showDropDown, setShowDropDown] = useState(false);
|
||||
const searchRef = useRef<InputRef>(null);
|
||||
const { logLinesPerPage, idEnd, idStart, liveTail } = useSelector<
|
||||
const { logLinesPerPage, idEnd, idStart, liveTail, order } = useSelector<
|
||||
AppState,
|
||||
ILogsReducer
|
||||
>((state) => state.logs);
|
||||
@ -99,11 +100,10 @@ function SearchFilter({
|
||||
q: customQuery,
|
||||
limit: logLinesPerPage,
|
||||
orderBy: 'timestamp',
|
||||
order: 'desc',
|
||||
order,
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
...getIdConditions(idStart, idEnd, order),
|
||||
});
|
||||
|
||||
getLogsAggregate({
|
||||
@ -128,6 +128,7 @@ function SearchFilter({
|
||||
logLinesPerPage,
|
||||
globalTime,
|
||||
getLogsFields,
|
||||
order,
|
||||
],
|
||||
);
|
||||
|
||||
@ -160,6 +161,7 @@ function SearchFilter({
|
||||
dispatch,
|
||||
globalTime.maxTime,
|
||||
globalTime.minTime,
|
||||
order,
|
||||
]);
|
||||
|
||||
const onPopOverChange = useCallback(
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import history from 'lib/history';
|
||||
@ -25,6 +26,7 @@ export function useSearchParser(): {
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const {
|
||||
searchFilter: { parsedQuery, queryString },
|
||||
order,
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
|
||||
const urlQuery = useUrlQuery();
|
||||
@ -39,7 +41,7 @@ export function useSearchParser(): {
|
||||
(updatedQueryString: string) => {
|
||||
history.replace({
|
||||
pathname: history.location.pathname,
|
||||
search: `?q=${updatedQueryString}`,
|
||||
search: `?${QueryParams.q}=${updatedQueryString}&${QueryParams.order}=${order}`,
|
||||
});
|
||||
|
||||
const globalTime = getMinMax(selectedTime, minTime, maxTime);
|
||||
|
@ -2,6 +2,8 @@ import {
|
||||
initialFormulaBuilderFormValues,
|
||||
initialQueryBuilderFormValuesMap,
|
||||
} from 'constants/queryBuilder';
|
||||
import getStep from 'lib/getStep';
|
||||
import store from 'store';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
@ -24,6 +26,11 @@ export const getQueryBuilderQueries = ({
|
||||
groupBy,
|
||||
aggregateAttribute: metricName,
|
||||
legend,
|
||||
stepInterval: getStep({
|
||||
end: store.getState().globalTime.maxTime,
|
||||
inputFormat: 'ns',
|
||||
start: store.getState().globalTime.minTime,
|
||||
}),
|
||||
reduceTo: 'sum',
|
||||
filters: {
|
||||
items: itemsA,
|
||||
@ -64,6 +71,11 @@ export const getQueryBuilderQuerieswithFormula = ({
|
||||
items: additionalItemsA,
|
||||
op: 'AND',
|
||||
},
|
||||
stepInterval: getStep({
|
||||
end: store.getState().globalTime.maxTime,
|
||||
inputFormat: 'ns',
|
||||
start: store.getState().globalTime.minTime,
|
||||
}),
|
||||
},
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.metrics,
|
||||
@ -79,6 +91,11 @@ export const getQueryBuilderQuerieswithFormula = ({
|
||||
items: additionalItemsB,
|
||||
op: 'AND',
|
||||
},
|
||||
stepInterval: getStep({
|
||||
end: store.getState().globalTime.maxTime,
|
||||
inputFormat: 'ns',
|
||||
start: store.getState().globalTime.minTime,
|
||||
}),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -11,11 +11,11 @@ import {
|
||||
} from 'hooks/useResourceAttribute/utils';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
||||
import { Button } from './styles';
|
||||
import {
|
||||
@ -25,7 +25,7 @@ import {
|
||||
onViewTracePopupClick,
|
||||
} from './util';
|
||||
|
||||
function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
||||
function DBCall(): JSX.Element {
|
||||
const { servicename } = useParams<{ servicename?: string }>();
|
||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||
const { queries } = useResourceAttribute();
|
||||
@ -59,7 +59,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
const databaseCallsAverageDurationWidget = useMemo(
|
||||
() =>
|
||||
@ -73,7 +73,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -151,8 +151,4 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
interface DBCallProps {
|
||||
getWidgetQueryBuilder: (query: Widgets['query']) => Widgets;
|
||||
}
|
||||
|
||||
export default DBCall;
|
||||
|
@ -13,10 +13,10 @@ import {
|
||||
} from 'hooks/useResourceAttribute/utils';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
||||
import { legend } from './constant';
|
||||
import { Button } from './styles';
|
||||
@ -26,7 +26,7 @@ import {
|
||||
onViewTracePopupClick,
|
||||
} from './util';
|
||||
|
||||
function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
function External(): JSX.Element {
|
||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||
|
||||
const { servicename } = useParams<{ servicename?: string }>();
|
||||
@ -51,7 +51,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const selectedTraceTags = useMemo(
|
||||
@ -71,7 +71,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const externalCallRPSWidget = useMemo(
|
||||
@ -87,7 +87,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
const externalCallDurationAddressWidget = useMemo(
|
||||
@ -103,7 +103,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
||||
[servicename, tagFilterItems],
|
||||
);
|
||||
|
||||
return (
|
||||
@ -261,8 +261,4 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
interface ExternalProps {
|
||||
getWidgetQueryBuilder: (query: Widgets['query']) => Widgets;
|
||||
}
|
||||
|
||||
export default External;
|
||||
|
@ -18,11 +18,11 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation, useParams } from 'react-router-dom';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import MetricReducer from 'types/reducer/metrics';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||
import {
|
||||
errorPercentage,
|
||||
operationPerSec,
|
||||
@ -36,7 +36,7 @@ import {
|
||||
onViewTracePopupClick,
|
||||
} from './util';
|
||||
|
||||
function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
||||
function Application(): JSX.Element {
|
||||
const { servicename } = useParams<{ servicename?: string }>();
|
||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||
const { search } = useLocation();
|
||||
@ -94,7 +94,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[getWidgetQueryBuilder, servicename, topLevelOperations, tagFilterItems],
|
||||
[servicename, topLevelOperations, tagFilterItems],
|
||||
);
|
||||
|
||||
const errorPercentageWidget = useMemo(
|
||||
@ -110,7 +110,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
||||
clickhouse_sql: [],
|
||||
id: uuid(),
|
||||
}),
|
||||
[servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder],
|
||||
[servicename, topLevelOperations, tagFilterItems],
|
||||
);
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
@ -289,10 +289,6 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
interface DashboardProps {
|
||||
getWidgetQueryBuilder: (query: Widgets['query']) => Widgets;
|
||||
}
|
||||
|
||||
type ClickHandlerType = (
|
||||
ChartEvent: ChartEvent,
|
||||
activeElements: ActiveElement[],
|
||||
|
@ -6,21 +6,20 @@ import { memo, useMemo } from 'react';
|
||||
import { generatePath, useParams } from 'react-router-dom';
|
||||
import { useLocation } from 'react-use';
|
||||
|
||||
import { getWidgetQueryBuilder } from './MetricsApplication.factory';
|
||||
import DBCall from './Tabs/DBCall';
|
||||
import External from './Tabs/External';
|
||||
import Overview from './Tabs/Overview';
|
||||
|
||||
function OverViewTab(): JSX.Element {
|
||||
return <Overview getWidgetQueryBuilder={getWidgetQueryBuilder} />;
|
||||
return <Overview />;
|
||||
}
|
||||
|
||||
function DbCallTab(): JSX.Element {
|
||||
return <DBCall getWidgetQueryBuilder={getWidgetQueryBuilder} />;
|
||||
return <DBCall />;
|
||||
}
|
||||
|
||||
function ExternalTab(): JSX.Element {
|
||||
return <External getWidgetQueryBuilder={getWidgetQueryBuilder} />;
|
||||
return <External />;
|
||||
}
|
||||
|
||||
function ServiceMetrics(): JSX.Element {
|
||||
|
@ -47,7 +47,9 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
|
||||
history.push(
|
||||
`${history.location.pathname}/new?graphType=${name}&widgetId=${
|
||||
emptyLayout.i
|
||||
}&${COMPOSITE_QUERY}=${JSON.stringify(initialQueriesMap.metrics)}`,
|
||||
}&${COMPOSITE_QUERY}=${encodeURIComponent(
|
||||
JSON.stringify(initialQueriesMap.metrics),
|
||||
)}`,
|
||||
);
|
||||
} catch (error) {
|
||||
notifications.error({
|
||||
|
@ -16,7 +16,13 @@ const Items: ItemsProps[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export type ITEMS = 'graph' | 'value' | 'list' | 'table' | 'EMPTY_WIDGET';
|
||||
export type ITEMS =
|
||||
| 'graph'
|
||||
| 'value'
|
||||
| 'list'
|
||||
| 'table'
|
||||
| 'EMPTY_WIDGET'
|
||||
| 'trace';
|
||||
|
||||
interface ItemsProps {
|
||||
name: ITEMS;
|
||||
|
@ -6,6 +6,7 @@ import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useCallback } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
@ -22,6 +23,7 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import DashboardReducer from 'types/reducer/dashboards';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
|
||||
import PromQLQueryContainer from './QueryBuilder/promQL';
|
||||
@ -33,6 +35,11 @@ function QuerySection({
|
||||
}: QueryProps): JSX.Element {
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
);
|
||||
@ -58,7 +65,7 @@ function QuerySection({
|
||||
|
||||
const { query } = selectedWidget;
|
||||
|
||||
useShareBuilderUrl({ defaultValue: query });
|
||||
useShareBuilderUrl(query);
|
||||
|
||||
const handleStageQuery = useCallback(
|
||||
(updatedQuery: Query): void => {
|
||||
@ -67,10 +74,19 @@ function QuerySection({
|
||||
yAxisUnit: selectedWidget.yAxisUnit,
|
||||
});
|
||||
|
||||
redirectWithQueryBuilderData(updatedQuery);
|
||||
redirectWithQueryBuilderData(
|
||||
updateStepInterval(updatedQuery, maxTime, minTime),
|
||||
);
|
||||
},
|
||||
|
||||
[urlQuery, selectedWidget, updateQuery, redirectWithQueryBuilderData],
|
||||
[
|
||||
updateQuery,
|
||||
urlQuery,
|
||||
selectedWidget.yAxisUnit,
|
||||
redirectWithQueryBuilderData,
|
||||
maxTime,
|
||||
minTime,
|
||||
],
|
||||
);
|
||||
|
||||
const handleQueryCategoryChange = (qCategory: string): void => {
|
||||
|
@ -1,11 +1,18 @@
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Input } from 'antd';
|
||||
import Typography from 'antd/es/typography/Typography';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { OptionsMenuConfig } from '..';
|
||||
import { FieldTitle } from '../styles';
|
||||
import { AddColumnSelect, AddColumnWrapper, SearchIconWrapper } from './styles';
|
||||
import { OptionsMenuConfig } from '../types';
|
||||
import {
|
||||
AddColumnItem,
|
||||
AddColumnSelect,
|
||||
AddColumnWrapper,
|
||||
DeleteOutlinedIcon,
|
||||
SearchIconWrapper,
|
||||
} from './styles';
|
||||
|
||||
function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
|
||||
const { t } = useTranslation(['trace']);
|
||||
@ -19,19 +26,24 @@ function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
|
||||
|
||||
<Input.Group compact>
|
||||
<AddColumnSelect
|
||||
allowClear
|
||||
maxTagCount={0}
|
||||
size="small"
|
||||
mode="multiple"
|
||||
placeholder="Search"
|
||||
options={config.options}
|
||||
value={config.value}
|
||||
value={[]}
|
||||
onChange={config.onChange}
|
||||
/>
|
||||
<SearchIconWrapper $isDarkMode={isDarkMode}>
|
||||
<SearchOutlined />
|
||||
</SearchIconWrapper>
|
||||
</Input.Group>
|
||||
|
||||
{config.value?.map(({ key, id }) => (
|
||||
<AddColumnItem direction="horizontal" key={id}>
|
||||
<Typography>{key}</Typography>
|
||||
<DeleteOutlinedIcon onClick={(): void => config.onRemove(id as string)} />
|
||||
</AddColumnItem>
|
||||
))}
|
||||
</AddColumnWrapper>
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
import { Card, Select, SelectProps, Space } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { FunctionComponent } from 'react';
|
||||
@ -26,3 +27,13 @@ export const AddColumnSelect: FunctionComponent<SelectProps> = styled(
|
||||
export const AddColumnWrapper = styled(Space)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const AddColumnItem = styled(Space)`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export const DeleteOutlinedIcon = styled(DeleteOutlined)`
|
||||
color: red;
|
||||
`;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { OptionsMenuConfig } from '..';
|
||||
import { FieldTitle } from '../styles';
|
||||
import { OptionsMenuConfig } from '../types';
|
||||
import { FormatFieldWrapper, RadioButton, RadioGroup } from './styles';
|
||||
|
||||
function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { OptionsMenuConfig } from '..';
|
||||
import { FieldTitle } from '../styles';
|
||||
import { OptionsMenuConfig } from '../types';
|
||||
import { MaxLinesFieldWrapper, MaxLinesInput } from './styles';
|
||||
|
||||
function MaxLinesField({ config }: MaxLinesFieldProps): JSX.Element | null {
|
||||
|
9
frontend/src/container/OptionsMenu/constants.ts
Normal file
9
frontend/src/container/OptionsMenu/constants.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { OptionsQuery } from './types';
|
||||
|
||||
export const URL_OPTIONS = 'options';
|
||||
|
||||
export const defaultOptionsQuery: OptionsQuery = {
|
||||
selectColumns: [],
|
||||
maxLines: 0,
|
||||
format: 'default',
|
||||
};
|
@ -1,11 +1,5 @@
|
||||
import { SettingFilled, SettingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
InputNumberProps,
|
||||
Popover,
|
||||
RadioProps,
|
||||
SelectProps,
|
||||
Space,
|
||||
} from 'antd';
|
||||
import { Popover, Space } from 'antd';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -14,6 +8,12 @@ import AddColumnField from './AddColumnField';
|
||||
import FormatField from './FormatField';
|
||||
import MaxLinesField from './MaxLinesField';
|
||||
import { OptionsContainer, OptionsContentWrapper } from './styles';
|
||||
import { OptionsMenuConfig } from './types';
|
||||
import useOptionsMenu from './useOptionsMenu';
|
||||
|
||||
interface OptionsMenuProps {
|
||||
config: OptionsMenuConfig;
|
||||
}
|
||||
|
||||
function OptionsMenu({ config }: OptionsMenuProps): JSX.Element {
|
||||
const { t } = useTranslation(['trace']);
|
||||
@ -44,14 +44,6 @@ function OptionsMenu({ config }: OptionsMenuProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export type OptionsMenuConfig = {
|
||||
format?: Pick<RadioProps, 'value' | 'onChange'>;
|
||||
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
||||
addColumn?: Pick<SelectProps, 'options' | 'value' | 'onChange'>;
|
||||
};
|
||||
|
||||
interface OptionsMenuProps {
|
||||
config: OptionsMenuConfig;
|
||||
}
|
||||
|
||||
export default OptionsMenu;
|
||||
|
||||
export { useOptionsMenu };
|
||||
|
22
frontend/src/container/OptionsMenu/types.ts
Normal file
22
frontend/src/container/OptionsMenu/types.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { InputNumberProps, RadioProps, SelectProps } from 'antd';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export interface OptionsQuery {
|
||||
selectColumns: BaseAutocompleteData[];
|
||||
maxLines: number;
|
||||
format: 'default' | 'row' | 'column';
|
||||
}
|
||||
|
||||
export interface InitialOptions
|
||||
extends Omit<Partial<OptionsQuery>, 'selectColumns'> {
|
||||
selectColumns?: string[];
|
||||
}
|
||||
|
||||
export type OptionsMenuConfig = {
|
||||
format?: Pick<RadioProps, 'value' | 'onChange'>;
|
||||
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
||||
addColumn?: Pick<SelectProps, 'options' | 'onChange'> & {
|
||||
value: BaseAutocompleteData[];
|
||||
onRemove: (key: string) => void;
|
||||
};
|
||||
};
|
178
frontend/src/container/OptionsMenu/useOptionsMenu.ts
Normal file
178
frontend/src/container/OptionsMenu/useOptionsMenu.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import { RadioChangeEvent } from 'antd';
|
||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { defaultOptionsQuery, URL_OPTIONS } from './constants';
|
||||
import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types';
|
||||
import { getInitialColumns, getOptionsFromKeys } from './utils';
|
||||
|
||||
interface UseOptionsMenuProps {
|
||||
dataSource: DataSource;
|
||||
aggregateOperator: string;
|
||||
initialOptions?: InitialOptions;
|
||||
}
|
||||
|
||||
interface UseOptionsMenu {
|
||||
isLoading: boolean;
|
||||
options: OptionsQuery;
|
||||
config: OptionsMenuConfig;
|
||||
}
|
||||
|
||||
const useOptionsMenu = ({
|
||||
dataSource,
|
||||
aggregateOperator,
|
||||
initialOptions = {},
|
||||
}: UseOptionsMenuProps): UseOptionsMenu => {
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const {
|
||||
query: optionsQuery,
|
||||
queryData: optionsQueryData,
|
||||
redirectWithQuery: redirectWithOptionsData,
|
||||
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS);
|
||||
|
||||
const { data, isFetched, isLoading } = useQuery(
|
||||
[QueryBuilderKeys.GET_ATTRIBUTE_KEY],
|
||||
async () =>
|
||||
getAggregateKeys({
|
||||
searchText: '',
|
||||
dataSource,
|
||||
aggregateOperator,
|
||||
aggregateAttribute: '',
|
||||
}),
|
||||
);
|
||||
|
||||
const attributeKeys = useMemo(() => data?.payload?.attributeKeys || [], [
|
||||
data?.payload?.attributeKeys,
|
||||
]);
|
||||
|
||||
const initialOptionsQuery: OptionsQuery = useMemo(
|
||||
() => ({
|
||||
...defaultOptionsQuery,
|
||||
...initialOptions,
|
||||
selectColumns: initialOptions?.selectColumns
|
||||
? getInitialColumns(initialOptions?.selectColumns || [], attributeKeys)
|
||||
: defaultOptionsQuery.selectColumns,
|
||||
}),
|
||||
[initialOptions, attributeKeys],
|
||||
);
|
||||
|
||||
const selectedColumnKeys = useMemo(
|
||||
() => optionsQueryData?.selectColumns?.map(({ id }) => id) || [],
|
||||
[optionsQueryData],
|
||||
);
|
||||
|
||||
const addColumnOptions = useMemo(
|
||||
() => getOptionsFromKeys(attributeKeys, selectedColumnKeys),
|
||||
[attributeKeys, selectedColumnKeys],
|
||||
);
|
||||
|
||||
const handleSelectedColumnsChange = useCallback(
|
||||
(value: string[]) => {
|
||||
const newSelectedColumnKeys = [
|
||||
...new Set([...selectedColumnKeys, ...value]),
|
||||
];
|
||||
const newSelectedColumns = newSelectedColumnKeys.reduce((acc, key) => {
|
||||
const column = attributeKeys.find(({ id }) => id === key);
|
||||
|
||||
if (!column) return acc;
|
||||
return [...acc, column];
|
||||
}, [] as BaseAutocompleteData[]);
|
||||
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: newSelectedColumns,
|
||||
});
|
||||
},
|
||||
[attributeKeys, selectedColumnKeys, redirectWithOptionsData],
|
||||
);
|
||||
|
||||
const handleRemoveSelectedColumn = useCallback(
|
||||
(columnKey: string) => {
|
||||
const newSelectedColumns = optionsQueryData?.selectColumns?.filter(
|
||||
({ id }) => id !== columnKey,
|
||||
);
|
||||
|
||||
if (!newSelectedColumns.length) {
|
||||
notifications.error({
|
||||
message: 'There must be at least one selected column',
|
||||
});
|
||||
} else {
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
selectColumns: newSelectedColumns,
|
||||
});
|
||||
}
|
||||
},
|
||||
[optionsQueryData, notifications, redirectWithOptionsData],
|
||||
);
|
||||
|
||||
const handleFormatChange = useCallback(
|
||||
(event: RadioChangeEvent) => {
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
format: event.target.value,
|
||||
});
|
||||
},
|
||||
[redirectWithOptionsData],
|
||||
);
|
||||
|
||||
const handleMaxLinesChange = useCallback(
|
||||
(value: string | number | null) => {
|
||||
redirectWithOptionsData({
|
||||
...defaultOptionsQuery,
|
||||
maxLines: value as number,
|
||||
});
|
||||
},
|
||||
[redirectWithOptionsData],
|
||||
);
|
||||
|
||||
const optionsMenuConfig: Required<OptionsMenuConfig> = useMemo(
|
||||
() => ({
|
||||
addColumn: {
|
||||
value: optionsQueryData?.selectColumns || defaultOptionsQuery.selectColumns,
|
||||
options: addColumnOptions || [],
|
||||
onChange: handleSelectedColumnsChange,
|
||||
onRemove: handleRemoveSelectedColumn,
|
||||
},
|
||||
format: {
|
||||
value: optionsQueryData?.format || defaultOptionsQuery.format,
|
||||
onChange: handleFormatChange,
|
||||
},
|
||||
maxLines: {
|
||||
value: optionsQueryData?.maxLines || defaultOptionsQuery.maxLines,
|
||||
onChange: handleMaxLinesChange,
|
||||
},
|
||||
}),
|
||||
[
|
||||
addColumnOptions,
|
||||
optionsQueryData?.maxLines,
|
||||
optionsQueryData?.format,
|
||||
optionsQueryData?.selectColumns,
|
||||
handleSelectedColumnsChange,
|
||||
handleRemoveSelectedColumn,
|
||||
handleFormatChange,
|
||||
handleMaxLinesChange,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (optionsQuery || !isFetched) return;
|
||||
|
||||
redirectWithOptionsData(initialOptionsQuery);
|
||||
}, [isFetched, optionsQuery, initialOptionsQuery, redirectWithOptionsData]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
options: optionsQueryData,
|
||||
config: optionsMenuConfig,
|
||||
};
|
||||
};
|
||||
|
||||
export default useOptionsMenu;
|
28
frontend/src/container/OptionsMenu/utils.ts
Normal file
28
frontend/src/container/OptionsMenu/utils.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { SelectProps } from 'antd';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
|
||||
export const getOptionsFromKeys = (
|
||||
keys: BaseAutocompleteData[],
|
||||
selectedKeys: (string | undefined)[],
|
||||
): SelectProps['options'] => {
|
||||
const options = keys.map(({ id, key }) => ({
|
||||
label: key,
|
||||
value: id,
|
||||
}));
|
||||
|
||||
return options.filter(
|
||||
({ value }) => !selectedKeys.find((key) => key === value),
|
||||
);
|
||||
};
|
||||
|
||||
export const getInitialColumns = (
|
||||
initialColumnTitles: string[],
|
||||
attributeKeys: BaseAutocompleteData[],
|
||||
): BaseAutocompleteData[] =>
|
||||
initialColumnTitles.reduce((acc, title) => {
|
||||
const initialColumn = attributeKeys.find(({ key }) => title === key);
|
||||
|
||||
if (!initialColumn) return acc;
|
||||
|
||||
return [...acc, initialColumn];
|
||||
}, [] as BaseAutocompleteData[]);
|
@ -15,26 +15,36 @@ import { ActionsWrapperStyled } from './QueryBuilder.styled';
|
||||
|
||||
export const QueryBuilder = memo(function QueryBuilder({
|
||||
config,
|
||||
panelType,
|
||||
panelType: newPanelType,
|
||||
actions,
|
||||
}: QueryBuilderProps): JSX.Element {
|
||||
const {
|
||||
currentQuery,
|
||||
setupInitialDataSource,
|
||||
addNewBuilderQuery,
|
||||
addNewFormula,
|
||||
handleSetPanelType,
|
||||
handleSetConfig,
|
||||
panelType,
|
||||
initialDataSource,
|
||||
} = useQueryBuilder();
|
||||
|
||||
useEffect(() => {
|
||||
if (config && config.queryVariant === 'static') {
|
||||
setupInitialDataSource(config.initialDataSource);
|
||||
}
|
||||
}, [config, setupInitialDataSource]);
|
||||
const currentDataSource = useMemo(
|
||||
() =>
|
||||
(config && config.queryVariant === 'static' && config.initialDataSource) ||
|
||||
null,
|
||||
[config],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
handleSetPanelType(panelType);
|
||||
}, [handleSetPanelType, panelType]);
|
||||
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
|
||||
handleSetConfig(newPanelType, currentDataSource);
|
||||
}
|
||||
}, [
|
||||
handleSetConfig,
|
||||
panelType,
|
||||
initialDataSource,
|
||||
currentDataSource,
|
||||
newPanelType,
|
||||
]);
|
||||
|
||||
const isDisabledQueryButton = useMemo(
|
||||
() => currentQuery.builder.queryData.length >= MAX_QUERIES,
|
||||
@ -48,7 +58,7 @@ export const QueryBuilder = memo(function QueryBuilder({
|
||||
|
||||
const isAvailableToDisableQuery = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData.length > 1 ||
|
||||
currentQuery.builder.queryData.length > 0 ||
|
||||
currentQuery.builder.queryFormulas.length > 0,
|
||||
[currentQuery],
|
||||
);
|
||||
|
@ -40,6 +40,7 @@ export const Query = memo(function Query({
|
||||
const {
|
||||
operators,
|
||||
isMetricsDataSource,
|
||||
isTracePanelType,
|
||||
listOfAdditionalFilters,
|
||||
handleChangeAggregatorAttribute,
|
||||
handleChangeDataSource,
|
||||
@ -196,7 +197,56 @@ export const Query = memo(function Query({
|
||||
}
|
||||
|
||||
default: {
|
||||
return null;
|
||||
return (
|
||||
<>
|
||||
<Col span={11}>
|
||||
<Row gutter={[11, 5]}>
|
||||
<Col flex="5.93rem">
|
||||
<FilterLabel label="Limit" />
|
||||
</Col>
|
||||
<Col flex="1 1 12.5rem">
|
||||
<LimitFilter query={query} onChange={handleChangeLimit} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={11}>
|
||||
<Row gutter={[11, 5]}>
|
||||
<Col flex="5.93rem">
|
||||
<FilterLabel label="HAVING" />
|
||||
</Col>
|
||||
<Col flex="1 1 12.5rem">
|
||||
<HavingFilter onChange={handleChangeHavingFilter} query={query} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={11}>
|
||||
<Row gutter={[11, 5]}>
|
||||
<Col flex="5.93rem">
|
||||
<FilterLabel label="Order by" />
|
||||
</Col>
|
||||
<Col flex="1 1 12.5rem">
|
||||
<OrderByFilter query={query} onChange={handleChangeOrderByKeys} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
|
||||
{panelType !== PANEL_TYPES.LIST && (
|
||||
<Col span={11}>
|
||||
<Row gutter={[11, 5]}>
|
||||
<Col flex="5.93rem">
|
||||
<FilterLabel label="Aggregate Every" />
|
||||
</Col>
|
||||
<Col flex="1 1 6rem">
|
||||
<AggregateEveryFilter
|
||||
query={query}
|
||||
onChange={handleChangeAggregateEvery}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
@ -278,8 +328,11 @@ export const Query = memo(function Query({
|
||||
</Col>
|
||||
<Col flex="1 1 12.5rem">
|
||||
<AggregatorFilter
|
||||
onChange={handleChangeAggregatorAttribute}
|
||||
query={query}
|
||||
onChange={handleChangeAggregatorAttribute}
|
||||
disabled={
|
||||
panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@ -305,6 +358,7 @@ export const Query = memo(function Query({
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
{!isTracePanelType && (
|
||||
<Col span={24}>
|
||||
<AdditionalFiltersToggler listOfAdditionalFilter={listOfAdditionalFilters}>
|
||||
<Row gutter={[0, 11]} justify="space-between">
|
||||
@ -312,6 +366,8 @@ export const Query = memo(function Query({
|
||||
</Row>
|
||||
</AdditionalFiltersToggler>
|
||||
</Col>
|
||||
)}
|
||||
{panelType !== PANEL_TYPES.LIST && panelType !== PANEL_TYPES.TRACE && (
|
||||
<Row style={{ width: '100%' }}>
|
||||
<Input
|
||||
onChange={handleChangeQueryLegend}
|
||||
@ -320,6 +376,7 @@ export const Query = memo(function Query({
|
||||
addonBefore="Legend Format"
|
||||
/>
|
||||
</Row>
|
||||
)}
|
||||
</ListItemWrapper>
|
||||
);
|
||||
});
|
||||
|
@ -1,11 +1,7 @@
|
||||
import { Input } from 'antd';
|
||||
import getStep from 'lib/getStep';
|
||||
import { InputNumber, InputNumberProps } from 'antd';
|
||||
import { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
|
||||
@ -13,49 +9,27 @@ function AggregateEveryFilter({
|
||||
onChange,
|
||||
query,
|
||||
}: AggregateEveryFilterProps): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const stepInterval = useMemo(
|
||||
() =>
|
||||
getStep({
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
[maxTime, minTime],
|
||||
);
|
||||
|
||||
const handleKeyDown = (event: {
|
||||
keyCode: number;
|
||||
which: number;
|
||||
preventDefault: () => void;
|
||||
}): void => {
|
||||
const keyCode = event.keyCode || event.which;
|
||||
const isBackspace = keyCode === 8;
|
||||
const isNumeric =
|
||||
(keyCode >= 48 && keyCode <= 57) || (keyCode >= 96 && keyCode <= 105);
|
||||
|
||||
if (!isNumeric && !isBackspace) {
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const isMetricsDataSource = useMemo(
|
||||
() => query.dataSource === DataSource.METRICS,
|
||||
[query.dataSource],
|
||||
);
|
||||
|
||||
const onChangeHandler: InputNumberProps<number>['onChange'] = (event) => {
|
||||
if (event && event >= 0) {
|
||||
onChange(event);
|
||||
}
|
||||
};
|
||||
|
||||
const isDisabled = isMetricsDataSource && !query.aggregateAttribute.key;
|
||||
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
<InputNumber
|
||||
placeholder="Enter in seconds"
|
||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
||||
disabled={isDisabled}
|
||||
style={selectStyle}
|
||||
defaultValue={query.stepInterval ?? stepInterval}
|
||||
onChange={(event): void => onChange(Number(event.target.value))}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={query.stepInterval}
|
||||
onChange={onChangeHandler}
|
||||
min={0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { AutoCompleteProps } from 'antd';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export type AgregatorFilterProps = {
|
||||
onChange: (value: BaseAutocompleteData) => void;
|
||||
export type AgregatorFilterProps = Pick<AutoCompleteProps, 'disabled'> & {
|
||||
query: IBuilderQuery;
|
||||
onChange: (value: BaseAutocompleteData) => void;
|
||||
};
|
||||
|
@ -28,8 +28,9 @@ import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import { AgregatorFilterProps } from './AggregatorFilter.intefaces';
|
||||
|
||||
export const AggregatorFilter = memo(function AggregatorFilter({
|
||||
onChange,
|
||||
query,
|
||||
disabled,
|
||||
onChange,
|
||||
}: AgregatorFilterProps): JSX.Element {
|
||||
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
|
||||
const debouncedValue = useDebounce(query.aggregateAttribute.key, 300);
|
||||
@ -119,6 +120,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
||||
options={optionsData}
|
||||
value={value}
|
||||
onChange={handleChangeAttribute}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
@ -102,10 +102,12 @@ export const GroupByFilter = memo(function GroupByFilter({
|
||||
|
||||
return {
|
||||
id,
|
||||
key,
|
||||
dataType: dataType as DataType,
|
||||
type: type as AutocompleteType,
|
||||
isColumn: isColumn === 'true',
|
||||
key: key || currentValue,
|
||||
dataType: (dataType as DataType) || initialAutocompleteData.dataType,
|
||||
type: (type as AutocompleteType) || initialAutocompleteData.type,
|
||||
isColumn: isColumn
|
||||
? isColumn === 'true'
|
||||
: initialAutocompleteData.isColumn,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Select, Spin } from 'antd';
|
||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||
import { IOption } from 'hooks/useResourceAttribute/types';
|
||||
import { uniqWith } from 'lodash-es';
|
||||
import * as Papa from 'papaparse';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
@ -9,12 +11,14 @@ import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import { getRemoveOrderFromValue } from '../QueryBuilderSearch/utils';
|
||||
import { FILTERS } from './config';
|
||||
import { OrderByFilterProps } from './OrderByFilter.interfaces';
|
||||
import {
|
||||
checkIfKeyPresent,
|
||||
getLabelFromValue,
|
||||
mapLabelValuePairs,
|
||||
orderByValueDelimiter,
|
||||
transformToOrderByStringValues,
|
||||
} from './utils';
|
||||
|
||||
export function OrderByFilter({
|
||||
@ -22,7 +26,9 @@ export function OrderByFilter({
|
||||
onChange,
|
||||
}: OrderByFilterProps): JSX.Element {
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [selectedValue, setSelectedValue] = useState<string[]>([]);
|
||||
const [selectedValue, setSelectedValue] = useState<IOption[]>(
|
||||
transformToOrderByStringValues(query.orderBy) || [],
|
||||
);
|
||||
|
||||
const { data, isFetching } = useQuery(
|
||||
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText],
|
||||
@ -55,23 +61,41 @@ export function OrderByFilter({
|
||||
.flat()
|
||||
.concat([
|
||||
{
|
||||
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) asc`,
|
||||
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}asc`,
|
||||
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.ASC}`,
|
||||
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}${FILTERS.ASC}`,
|
||||
},
|
||||
{
|
||||
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) desc`,
|
||||
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}desc`,
|
||||
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.DESC}`,
|
||||
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}${FILTERS.DESC}`,
|
||||
},
|
||||
]),
|
||||
[query.aggregateAttribute.key, query.aggregateOperator, query.groupBy],
|
||||
);
|
||||
|
||||
const customValue: IOption[] = useMemo(() => {
|
||||
if (!searchText) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
label: `${searchText} ${FILTERS.ASC}`,
|
||||
value: `${searchText}${orderByValueDelimiter}${FILTERS.ASC}`,
|
||||
},
|
||||
{
|
||||
label: `${searchText} ${FILTERS.DESC}`,
|
||||
value: `${searchText}${orderByValueDelimiter}${FILTERS.DESC}`,
|
||||
},
|
||||
];
|
||||
}, [searchText]);
|
||||
|
||||
const optionsData = useMemo(() => {
|
||||
const options =
|
||||
query.aggregateOperator === MetricAggregateOperator.NOOP
|
||||
? noAggregationOptions
|
||||
: aggregationOptions;
|
||||
return options.filter(
|
||||
|
||||
const resultOption = [...customValue, ...options];
|
||||
|
||||
return resultOption.filter(
|
||||
(option) =>
|
||||
!getLabelFromValue(selectedValue).includes(
|
||||
getRemoveOrderFromValue(option.value),
|
||||
@ -79,30 +103,58 @@ export function OrderByFilter({
|
||||
);
|
||||
}, [
|
||||
aggregationOptions,
|
||||
customValue,
|
||||
noAggregationOptions,
|
||||
query.aggregateOperator,
|
||||
selectedValue,
|
||||
]);
|
||||
|
||||
const handleChange = (values: string[]): void => {
|
||||
setSelectedValue(values);
|
||||
const orderByValues: OrderByPayload[] = values.map((item) => {
|
||||
const match = Papa.parse(item, { delimiter: '|' });
|
||||
const getUniqValues = useCallback((values: IOption[]): IOption[] => {
|
||||
const modifiedValues = values.map((item) => {
|
||||
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
|
||||
if (!match) return { label: item.label, value: item.value };
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
|
||||
const [_, order] = match.data.flat() as string[];
|
||||
if (order) return { label: item.label, value: item.value };
|
||||
|
||||
return {
|
||||
label: `${item.value} ${FILTERS.ASC}`,
|
||||
value: `${item.value}${orderByValueDelimiter}${FILTERS.ASC}`,
|
||||
};
|
||||
});
|
||||
|
||||
return uniqWith(
|
||||
modifiedValues,
|
||||
(current, next) =>
|
||||
getRemoveOrderFromValue(current.value) ===
|
||||
getRemoveOrderFromValue(next.value),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleChange = (values: IOption[]): void => {
|
||||
const result = getUniqValues(values);
|
||||
|
||||
setSelectedValue(result);
|
||||
const orderByValues: OrderByPayload[] = result.map((item) => {
|
||||
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
|
||||
|
||||
if (match) {
|
||||
const [columnName, order] = match.data.flat() as string[];
|
||||
return {
|
||||
columnName: checkIfKeyPresent(columnName, query.aggregateAttribute.key)
|
||||
? '#SIGNOZ_VALUE'
|
||||
: columnName,
|
||||
order,
|
||||
order: order ?? 'asc',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
columnName: item,
|
||||
order: '',
|
||||
columnName: item.value,
|
||||
order: 'asc',
|
||||
};
|
||||
});
|
||||
|
||||
setSearchText('');
|
||||
onChange(orderByValues);
|
||||
};
|
||||
|
||||
@ -126,6 +178,8 @@ export function OrderByFilter({
|
||||
showSearch
|
||||
disabled={isMetricsDataSource && isDisabledSelect}
|
||||
showArrow={false}
|
||||
value={selectedValue}
|
||||
labelInValue
|
||||
filterOption={false}
|
||||
options={optionsData}
|
||||
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||
|
@ -0,0 +1,4 @@
|
||||
export const FILTERS = {
|
||||
ASC: 'asc',
|
||||
DESC: 'desc',
|
||||
};
|
@ -2,9 +2,18 @@ import { IOption } from 'hooks/useResourceAttribute/types';
|
||||
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||
import * as Papa from 'papaparse';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const orderByValueDelimiter = '|';
|
||||
|
||||
export const transformToOrderByStringValues = (
|
||||
orderBy: OrderByPayload[],
|
||||
): IOption[] =>
|
||||
orderBy.map((item) => ({
|
||||
label: `${item.columnName} ${item.order}`,
|
||||
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
|
||||
}));
|
||||
|
||||
export function mapLabelValuePairs(
|
||||
arr: BaseAutocompleteData[],
|
||||
): Array<IOption>[] {
|
||||
@ -28,14 +37,15 @@ export function mapLabelValuePairs(
|
||||
});
|
||||
}
|
||||
|
||||
export function getLabelFromValue(arr: string[]): string[] {
|
||||
export function getLabelFromValue(arr: IOption[]): string[] {
|
||||
return arr.flat().map((item) => {
|
||||
const match = Papa.parse(item, { delimiter: orderByValueDelimiter });
|
||||
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
|
||||
if (match) {
|
||||
const [key] = match.data as string[];
|
||||
return key[0];
|
||||
}
|
||||
return item;
|
||||
|
||||
return item.value;
|
||||
});
|
||||
}
|
||||
|
||||
|
16
frontend/src/container/QueryTable/QueryTable.intefaces.ts
Normal file
16
frontend/src/container/QueryTable/QueryTable.intefaces.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TableProps } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { ReactNode } from 'react';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
export type QueryTableProps = Omit<
|
||||
TableProps<RowData>,
|
||||
'columns' | 'dataSource'
|
||||
> & {
|
||||
queryTableData: QueryDataV3[];
|
||||
query: Query;
|
||||
renderActionCell?: (record: RowData) => ReactNode;
|
||||
modifyColumns?: (columns: ColumnsType<RowData>) => ColumnsType<RowData>;
|
||||
};
|
57
frontend/src/container/QueryTable/QueryTable.tsx
Normal file
57
frontend/src/container/QueryTable/QueryTable.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import dayjs from 'dayjs';
|
||||
import {
|
||||
createTableColumnsFromQuery,
|
||||
RowData,
|
||||
} from 'lib/query/createTableColumnsFromQuery';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { QueryTableProps } from './QueryTable.intefaces';
|
||||
|
||||
export function QueryTable({
|
||||
queryTableData,
|
||||
query,
|
||||
renderActionCell,
|
||||
modifyColumns,
|
||||
...props
|
||||
}: QueryTableProps): JSX.Element {
|
||||
const { columns, dataSource } = useMemo(
|
||||
() =>
|
||||
createTableColumnsFromQuery({
|
||||
query,
|
||||
queryTableData,
|
||||
renderActionCell,
|
||||
}),
|
||||
[query, queryTableData, renderActionCell],
|
||||
);
|
||||
|
||||
const modifiedColumns = useMemo(() => {
|
||||
const currentColumns: ColumnsType<RowData> = columns.map((column) =>
|
||||
column.key === 'timestamp'
|
||||
? {
|
||||
...column,
|
||||
render: (_, record): string =>
|
||||
dayjs(new Date(record.timestamp)).format('MMM DD, YYYY, HH:mm:ss'),
|
||||
}
|
||||
: column,
|
||||
);
|
||||
|
||||
return currentColumns;
|
||||
}, [columns]);
|
||||
|
||||
const tableColumns = modifyColumns
|
||||
? modifyColumns(modifiedColumns)
|
||||
: modifiedColumns;
|
||||
|
||||
return (
|
||||
<ResizeTable
|
||||
columns={tableColumns}
|
||||
tableLayout="fixed"
|
||||
dataSource={dataSource}
|
||||
scroll={{ x: true }}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
1
frontend/src/container/QueryTable/index.ts
Normal file
1
frontend/src/container/QueryTable/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { QueryTable } from './QueryTable';
|
53
frontend/src/container/TimeSeriesView/TimeSeriesView.tsx
Normal file
53
frontend/src/container/TimeSeriesView/TimeSeriesView.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import Graph from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import { useMemo } from 'react';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { Container, ErrorText } from './styles';
|
||||
|
||||
function TimeSeriesView({
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
}: TimeSeriesViewProps): JSX.Element {
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: data?.payload?.data?.result || [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[data?.payload?.data?.result],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{isLoading && <Spinner height="50vh" size="small" tip="Loading..." />}
|
||||
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
|
||||
{!isLoading && !isError && (
|
||||
<Graph
|
||||
animate={false}
|
||||
data={chartData}
|
||||
name="tracesExplorerGraph"
|
||||
type="line"
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface TimeSeriesViewProps {
|
||||
data?: SuccessResponse<MetricRangePayloadProps>;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
TimeSeriesView.defaultProps = {
|
||||
data: undefined,
|
||||
};
|
||||
|
||||
export default TimeSeriesView;
|
55
frontend/src/container/TimeSeriesView/index.tsx
Normal file
55
frontend/src/container/TimeSeriesView/index.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import TimeSeriesView from './TimeSeriesView';
|
||||
|
||||
function TimeSeriesViewContainer({
|
||||
dataSource = DataSource.TRACES,
|
||||
}: TimeSeriesViewProps): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const { data, isLoading, isError } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap[dataSource],
|
||||
graphType: panelType || PANEL_TYPES.TIME_SERIES,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource,
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
],
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
);
|
||||
|
||||
return <TimeSeriesView isError={isError} isLoading={isLoading} data={data} />;
|
||||
}
|
||||
|
||||
interface TimeSeriesViewProps {
|
||||
dataSource?: DataSource;
|
||||
}
|
||||
|
||||
TimeSeriesViewContainer.defaultProps = {
|
||||
dataSource: DataSource.TRACES,
|
||||
};
|
||||
|
||||
export default TimeSeriesViewContainer;
|
@ -4,6 +4,9 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import dayjs, { Dayjs } from 'dayjs';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
@ -66,6 +69,8 @@ function DateTimeSelection({
|
||||
false,
|
||||
);
|
||||
|
||||
const { stagedQuery, initQueryBuilderData } = useQueryBuilder();
|
||||
|
||||
const { maxTime, minTime, selectedTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
@ -174,6 +179,14 @@ function DateTimeSelection({
|
||||
setRefreshButtonHidden(true);
|
||||
setCustomDTPickerVisible(true);
|
||||
}
|
||||
|
||||
if (!stagedQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { maxTime, minTime } = GetMinMax(value, getTime());
|
||||
|
||||
initQueryBuilderData(updateStepInterval(stagedQuery, maxTime, minTime));
|
||||
};
|
||||
|
||||
const onRefreshHandler = (): void => {
|
||||
|
@ -1,25 +1,51 @@
|
||||
import Controls from 'container/Controls';
|
||||
import Controls, { ControlsProps } from 'container/Controls';
|
||||
import OptionsMenu from 'container/OptionsMenu';
|
||||
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||
import useQueryPagination from 'hooks/queryPagination/useQueryPagination';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { Container } from './styles';
|
||||
|
||||
function TraceExplorerControls(): JSX.Element | null {
|
||||
const handleCountItemsPerPageChange = (): void => {};
|
||||
const handleNavigatePrevious = (): void => {};
|
||||
const handleNavigateNext = (): void => {};
|
||||
function TraceExplorerControls({
|
||||
isLoading,
|
||||
totalCount,
|
||||
perPageOptions,
|
||||
config,
|
||||
}: TraceExplorerControlsProps): JSX.Element | null {
|
||||
const {
|
||||
pagination,
|
||||
handleCountItemsPerPageChange,
|
||||
handleNavigateNext,
|
||||
handleNavigatePrevious,
|
||||
} = useQueryPagination(totalCount, perPageOptions);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{config && <OptionsMenu config={{ addColumn: config?.addColumn }} />}
|
||||
|
||||
<Controls
|
||||
isLoading={false}
|
||||
count={0}
|
||||
countPerPage={0}
|
||||
handleNavigatePrevious={handleNavigatePrevious}
|
||||
handleNavigateNext={handleNavigateNext}
|
||||
isLoading={isLoading}
|
||||
totalCount={totalCount}
|
||||
offset={pagination.offset}
|
||||
countPerPage={pagination.limit}
|
||||
perPageOptions={perPageOptions}
|
||||
handleCountItemsPerPageChange={handleCountItemsPerPageChange}
|
||||
handleNavigateNext={handleNavigateNext}
|
||||
handleNavigatePrevious={handleNavigatePrevious}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
TraceExplorerControls.defaultProps = {
|
||||
config: null,
|
||||
};
|
||||
|
||||
type TraceExplorerControlsProps = Pick<
|
||||
ControlsProps,
|
||||
'isLoading' | 'totalCount' | 'perPageOptions'
|
||||
> & {
|
||||
config?: OptionsMenuConfig | null;
|
||||
};
|
||||
|
||||
export default memo(TraceExplorerControls);
|
||||
|
11
frontend/src/container/TracesExplorer/ListView/configs.tsx
Normal file
11
frontend/src/container/TracesExplorer/ListView/configs.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
|
||||
|
||||
export const defaultSelectedColumns: string[] = [
|
||||
'name',
|
||||
'serviceName',
|
||||
'responseStatusCode',
|
||||
'httpMethod',
|
||||
'durationNano',
|
||||
];
|
||||
|
||||
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];
|
134
frontend/src/container/TracesExplorer/ListView/index.tsx
Normal file
134
frontend/src/container/TracesExplorer/ListView/index.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { QueryTable } from 'container/QueryTable';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination, URL_PAGINATION } from 'hooks/queryPagination';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import history from 'lib/history';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { HTMLAttributes, memo, useCallback, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import TraceExplorerControls from '../Controls';
|
||||
import { defaultSelectedColumns, PER_PAGE_OPTIONS } from './configs';
|
||||
import { Container, ErrorText, tableStyles } from './styles';
|
||||
import { getTraceLink, modifyColumns, transformDataWithDate } from './utils';
|
||||
|
||||
function ListView(): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const { options, config } = useOptionsMenu({
|
||||
dataSource: DataSource.TRACES,
|
||||
aggregateOperator: 'count',
|
||||
initialOptions: {
|
||||
selectColumns: defaultSelectedColumns,
|
||||
},
|
||||
});
|
||||
|
||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||
URL_PAGINATION,
|
||||
);
|
||||
|
||||
const { data, isFetching, isError } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap.traces,
|
||||
graphType: panelType || PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource: 'traces',
|
||||
},
|
||||
tableParams: {
|
||||
pagination: paginationQueryData,
|
||||
selectColumns: options?.selectColumns,
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
options?.selectColumns,
|
||||
],
|
||||
enabled:
|
||||
!!stagedQuery && panelType === PANEL_TYPES.LIST && !!options?.selectColumns,
|
||||
},
|
||||
);
|
||||
|
||||
const dataLength =
|
||||
data?.payload?.data?.newResult?.data?.result[0]?.list?.length;
|
||||
const totalCount = useMemo(() => dataLength || 0, [dataLength]);
|
||||
|
||||
const queryTableDataResult = data?.payload.data.newResult.data.result;
|
||||
const queryTableData = useMemo(() => queryTableDataResult || [], [
|
||||
queryTableDataResult,
|
||||
]);
|
||||
|
||||
const transformedQueryTableData = useMemo(
|
||||
() => transformDataWithDate(queryTableData),
|
||||
[queryTableData],
|
||||
);
|
||||
|
||||
const handleModifyColumns = useCallback(
|
||||
(columns: ColumnsType<RowData>) =>
|
||||
modifyColumns(columns, options?.selectColumns || []),
|
||||
[options?.selectColumns],
|
||||
);
|
||||
|
||||
const handleRow = useCallback(
|
||||
(record: RowData): HTMLAttributes<RowData> => ({
|
||||
onClick: (event): void => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
window.open(getTraceLink(record), '_blank');
|
||||
} else {
|
||||
history.push(getTraceLink(record));
|
||||
}
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<TraceExplorerControls
|
||||
isLoading={isFetching}
|
||||
totalCount={totalCount}
|
||||
config={config}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
/>
|
||||
|
||||
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
|
||||
|
||||
{!isError && (
|
||||
<QueryTable
|
||||
query={stagedQuery || initialQueriesMap.traces}
|
||||
queryTableData={transformedQueryTableData}
|
||||
modifyColumns={handleModifyColumns}
|
||||
loading={isFetching}
|
||||
pagination={false}
|
||||
style={tableStyles}
|
||||
onRow={handleRow}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ListView);
|
21
frontend/src/container/TracesExplorer/ListView/styles.ts
Normal file
21
frontend/src/container/TracesExplorer/ListView/styles.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Typography } from 'antd';
|
||||
import { CSSProperties } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const tableStyles: CSSProperties = {
|
||||
cursor: 'pointer',
|
||||
};
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
`;
|
||||
|
||||
export const ErrorText = styled(Typography)`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const DateText = styled(Typography)`
|
||||
min-width: 145px;
|
||||
`;
|
100
frontend/src/container/TracesExplorer/ListView/utils.tsx
Normal file
100
frontend/src/container/TracesExplorer/ListView/utils.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import { Tag } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import Typography from 'antd/es/typography/Typography';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
import { formUrlParams } from 'container/TraceDetail/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
import { DateText } from './styles';
|
||||
|
||||
export const transformDataWithDate = (data: QueryDataV3[]): QueryDataV3[] =>
|
||||
data.map((query) => ({
|
||||
...query,
|
||||
list:
|
||||
query?.list?.map((listItem) => ({
|
||||
...listItem,
|
||||
data: {
|
||||
...listItem?.data,
|
||||
date: listItem?.timestamp,
|
||||
},
|
||||
})) || null,
|
||||
}));
|
||||
|
||||
export const modifyColumns = (
|
||||
columns: ColumnsType<RowData>,
|
||||
selectedColumns: BaseAutocompleteData[],
|
||||
): ColumnsType<RowData> => {
|
||||
const initialColumns = columns.filter(({ key }) => {
|
||||
let isValidColumn = true;
|
||||
|
||||
const checkIsExistColumnByKey = (attributeKey: string): boolean =>
|
||||
!selectedColumns.find(({ key }) => key === attributeKey) &&
|
||||
attributeKey === key;
|
||||
|
||||
const isSelectedSpanId = checkIsExistColumnByKey('spanID');
|
||||
const isSelectedTraceId = checkIsExistColumnByKey('traceID');
|
||||
|
||||
if (isSelectedSpanId || isSelectedTraceId || key === 'date')
|
||||
isValidColumn = false;
|
||||
|
||||
return isValidColumn;
|
||||
});
|
||||
|
||||
const dateColumn = columns.find(({ key }) => key === 'date');
|
||||
|
||||
if (dateColumn) {
|
||||
initialColumns.unshift(dateColumn);
|
||||
}
|
||||
|
||||
return initialColumns.map((column) => {
|
||||
const key = column.key as string;
|
||||
|
||||
const getHttpMethodOrStatus = (value: string): JSX.Element => {
|
||||
if (value === 'N/A') {
|
||||
return <Typography>{value}</Typography>;
|
||||
}
|
||||
|
||||
return <Tag color="magenta">{value}</Tag>;
|
||||
};
|
||||
|
||||
if (key === 'durationNano') {
|
||||
return {
|
||||
...column,
|
||||
render: (duration: string): JSX.Element => (
|
||||
<Typography>{getMs(duration)}ms</Typography>
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'httpMethod' || key === 'responseStatusCode') {
|
||||
return {
|
||||
...column,
|
||||
render: getHttpMethodOrStatus,
|
||||
};
|
||||
}
|
||||
|
||||
if (key === 'date') {
|
||||
return {
|
||||
...column,
|
||||
width: 145,
|
||||
render: (date: string): JSX.Element => {
|
||||
const day = dayjs(date);
|
||||
return <DateText>{day.format('YYYY/MM/DD HH:mm:ss')}</DateText>;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return column;
|
||||
});
|
||||
};
|
||||
|
||||
export const getTraceLink = (record: RowData): string =>
|
||||
`${ROUTES.TRACE}/${record.traceID}${formUrlParams({
|
||||
spanId: record.spanID,
|
||||
levelUp: 0,
|
||||
levelDown: 0,
|
||||
})}`;
|
@ -1,7 +1,9 @@
|
||||
import { Button } from 'antd';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QueryBuilder } from 'container/QueryBuilder';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { memo } from 'react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { ButtonWrapper, Container } from './styles';
|
||||
@ -9,10 +11,12 @@ import { ButtonWrapper, Container } from './styles';
|
||||
function QuerySection(): JSX.Element {
|
||||
const { handleRunQuery } = useQueryBuilder();
|
||||
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<QueryBuilder
|
||||
panelType={PANEL_TYPES.TIME_SERIES}
|
||||
panelType={panelTypes}
|
||||
config={{
|
||||
queryVariant: 'static',
|
||||
initialDataSource: DataSource.TRACES,
|
||||
@ -29,4 +33,4 @@ function QuerySection(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
export default QuerySection;
|
||||
export default memo(QuerySection);
|
||||
|
@ -1,73 +0,0 @@
|
||||
import Graph from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import getChartData from 'lib/getChartData';
|
||||
import { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { Container, ErrorText } from './styles';
|
||||
|
||||
function TimeSeriesView(): JSX.Element {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const { data, isLoading, isError } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap.traces,
|
||||
graphType: 'graph',
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource: 'traces',
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
stagedQuery,
|
||||
maxTime,
|
||||
minTime,
|
||||
],
|
||||
enabled: !!stagedQuery,
|
||||
},
|
||||
);
|
||||
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
getChartData({
|
||||
queryData: [
|
||||
{
|
||||
queryData: data?.payload?.data?.result || [],
|
||||
},
|
||||
],
|
||||
}),
|
||||
[data],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{isLoading && <Spinner height="50vh" size="small" tip="Loading..." />}
|
||||
{isError && <ErrorText>{data?.error || 'Something went wrong'}</ErrorText>}
|
||||
{!isLoading && !isError && (
|
||||
<Graph
|
||||
animate={false}
|
||||
data={chartData}
|
||||
name="tracesExplorerGraph"
|
||||
type="line"
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default TimeSeriesView;
|
49
frontend/src/container/TracesExplorer/TracesView/configs.tsx
Normal file
49
frontend/src/container/TracesExplorer/TracesView/configs.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
import { Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getMs } from 'container/Trace/Filters/Panel/PanelBody/Duration/util';
|
||||
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
|
||||
import { generatePath } from 'react-router-dom';
|
||||
import { ListItem } from 'types/api/widgets/getQuery';
|
||||
|
||||
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];
|
||||
|
||||
export const columns: ColumnsType<ListItem['data']> = [
|
||||
{
|
||||
title: 'Root Service Name',
|
||||
dataIndex: 'subQuery.serviceName',
|
||||
key: 'serviceName',
|
||||
},
|
||||
{
|
||||
title: 'Root Operation Name',
|
||||
dataIndex: 'subQuery.name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: 'Root Duration (in ms)',
|
||||
dataIndex: 'subQuery.durationNano',
|
||||
key: 'durationNano',
|
||||
render: (duration: number): JSX.Element => (
|
||||
<Typography>{getMs(String(duration))}ms</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'No of Spans',
|
||||
dataIndex: 'span_count',
|
||||
key: 'span_count',
|
||||
},
|
||||
{
|
||||
title: 'TraceID',
|
||||
dataIndex: 'traceID',
|
||||
key: 'traceID',
|
||||
render: (traceID: string): JSX.Element => (
|
||||
<Typography.Link
|
||||
href={generatePath(ROUTES.TRACE_DETAIL, {
|
||||
id: traceID,
|
||||
})}
|
||||
>
|
||||
{traceID}
|
||||
</Typography.Link>
|
||||
),
|
||||
},
|
||||
];
|
87
frontend/src/container/TracesExplorer/TracesView/index.tsx
Normal file
87
frontend/src/container/TracesExplorer/TracesView/index.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import Typography from 'antd/es/typography/Typography';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { Pagination, URL_PAGINATION } from 'hooks/queryPagination';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import TraceExplorerControls from '../Controls';
|
||||
import { columns, PER_PAGE_OPTIONS } from './configs';
|
||||
import { ActionsContainer, Container } from './styles';
|
||||
|
||||
function TracesView(): JSX.Element {
|
||||
const { stagedQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const { queryData: paginationQueryData } = useUrlQueryData<Pagination>(
|
||||
URL_PAGINATION,
|
||||
);
|
||||
|
||||
const { data, isLoading } = useGetQueryRange(
|
||||
{
|
||||
query: stagedQuery || initialQueriesMap.traces,
|
||||
graphType: panelType || PANEL_TYPES.TRACE,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource: 'traces',
|
||||
},
|
||||
tableParams: {
|
||||
pagination: paginationQueryData,
|
||||
},
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
globalSelectedTime,
|
||||
maxTime,
|
||||
minTime,
|
||||
stagedQuery,
|
||||
panelType,
|
||||
paginationQueryData,
|
||||
],
|
||||
enabled: !!stagedQuery && panelType === PANEL_TYPES.TRACE,
|
||||
},
|
||||
);
|
||||
|
||||
const responseData = data?.payload?.data?.newResult?.data?.result[0]?.list;
|
||||
const tableData = useMemo(
|
||||
() => responseData?.map((listItem) => listItem.data),
|
||||
[responseData],
|
||||
);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ActionsContainer>
|
||||
<Typography>
|
||||
Showing up to X of the slowest traces form the selected time range
|
||||
</Typography>
|
||||
<TraceExplorerControls
|
||||
isLoading={isLoading}
|
||||
totalCount={responseData?.length || 0}
|
||||
perPageOptions={PER_PAGE_OPTIONS}
|
||||
/>
|
||||
</ActionsContainer>
|
||||
<ResizeTable
|
||||
loading={isLoading}
|
||||
columns={columns}
|
||||
tableLayout="fixed"
|
||||
dataSource={tableData}
|
||||
scroll={{ x: true }}
|
||||
pagination={false}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(TracesView);
|
13
frontend/src/container/TracesExplorer/TracesView/styles.ts
Normal file
13
frontend/src/container/TracesExplorer/TracesView/styles.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
`;
|
||||
|
||||
export const ActionsContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`;
|
16
frontend/src/hooks/dashboard/useGetAllDashboard.tsx
Normal file
16
frontend/src/hooks/dashboard/useGetAllDashboard.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import getAll from 'api/dashboard/getAll';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useQuery, UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/dashboard/getAll';
|
||||
|
||||
export const useGetAllDashboard = (): DashboardProps =>
|
||||
useQuery({
|
||||
queryFn: getAll,
|
||||
queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS,
|
||||
});
|
||||
|
||||
type DashboardProps = UseQueryResult<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse,
|
||||
unknown
|
||||
>;
|
14
frontend/src/hooks/dashboard/useUpdateDashboard.tsx
Normal file
14
frontend/src/hooks/dashboard/useUpdateDashboard.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import update from 'api/dashboard/update';
|
||||
import { useMutation, UseMutationResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
|
||||
export const useUpdateDashboard = (): UseUpdateDashboard => useMutation(update);
|
||||
|
||||
type UseUpdateDashboard = UseMutationResult<
|
||||
SuccessResponse<Dashboard> | ErrorResponse,
|
||||
unknown,
|
||||
Props,
|
||||
unknown
|
||||
>;
|
37
frontend/src/hooks/dashboard/utils.ts
Normal file
37
frontend/src/hooks/dashboard/utils.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const addEmptyWidgetInDashboardJSONWithQuery = (
|
||||
dashboard: Dashboard,
|
||||
query: Query,
|
||||
): Dashboard => ({
|
||||
...dashboard,
|
||||
data: {
|
||||
...dashboard.data,
|
||||
layout: [
|
||||
{
|
||||
i: 'empty',
|
||||
w: 6,
|
||||
x: 0,
|
||||
h: 2,
|
||||
y: 0,
|
||||
},
|
||||
...(dashboard?.data?.layout || []),
|
||||
],
|
||||
widgets: [
|
||||
...(dashboard?.data?.widgets || []),
|
||||
{
|
||||
id: 'empty',
|
||||
query,
|
||||
description: '',
|
||||
isStacked: false,
|
||||
nullZeroValues: '',
|
||||
opacity: '',
|
||||
title: '',
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
@ -8,7 +8,16 @@ export const useGetCompositeQueryParam = (): Query | null => {
|
||||
|
||||
return useMemo(() => {
|
||||
const compositeQuery = urlQuery.get(COMPOSITE_QUERY);
|
||||
let parsedCompositeQuery: Query | null = null;
|
||||
|
||||
return compositeQuery ? JSON.parse(compositeQuery) : null;
|
||||
try {
|
||||
if (!compositeQuery) return null;
|
||||
|
||||
parsedCompositeQuery = JSON.parse(decodeURIComponent(compositeQuery));
|
||||
} catch (e) {
|
||||
parsedCompositeQuery = null;
|
||||
}
|
||||
|
||||
return parsedCompositeQuery;
|
||||
}, [urlQuery]);
|
||||
};
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||
import { UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
@ -11,6 +10,7 @@ import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { useGetQueryRange } from './useGetQueryRange';
|
||||
import { useQueryBuilder } from './useQueryBuilder';
|
||||
|
||||
export const useGetWidgetQueryRange = (
|
||||
{
|
||||
@ -19,31 +19,29 @@ export const useGetWidgetQueryRange = (
|
||||
}: Pick<GetQueryResultsProps, 'graphType' | 'selectedTime'>,
|
||||
options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
|
||||
): UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error> => {
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||
AppState,
|
||||
GlobalReducer
|
||||
>((state) => state.globalTime);
|
||||
|
||||
const compositeQuery = urlQuery.get(COMPOSITE_QUERY);
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
|
||||
return useGetQueryRange(
|
||||
{
|
||||
graphType,
|
||||
selectedTime,
|
||||
globalSelectedInterval,
|
||||
query: JSON.parse(compositeQuery || '{}'),
|
||||
query: stagedQuery || initialQueriesMap.metrics,
|
||||
variables: getDashboardVariables(),
|
||||
},
|
||||
{
|
||||
enabled: !!compositeQuery,
|
||||
enabled: !!stagedQuery,
|
||||
retry: false,
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
selectedTime,
|
||||
globalSelectedInterval,
|
||||
compositeQuery,
|
||||
stagedQuery,
|
||||
],
|
||||
...options,
|
||||
},
|
||||
|
@ -2,6 +2,7 @@ import {
|
||||
initialAutocompleteData,
|
||||
initialQueryBuilderFormValuesMap,
|
||||
mapOfFilters,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
|
||||
@ -56,9 +57,16 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
|
||||
);
|
||||
|
||||
const getNewListOfAdditionalFilters = useCallback(
|
||||
(dataSource: DataSource): string[] =>
|
||||
mapOfFilters[dataSource].map((item) => item.text),
|
||||
[],
|
||||
(dataSource: DataSource): string[] => {
|
||||
const listOfFilters = mapOfFilters[dataSource].map((item) => item.text);
|
||||
|
||||
if (panelType === PANEL_TYPES.LIST) {
|
||||
return listOfFilters.filter((filter) => filter !== 'Aggregation interval');
|
||||
}
|
||||
|
||||
return listOfFilters;
|
||||
},
|
||||
[panelType],
|
||||
);
|
||||
|
||||
const handleChangeAggregatorAttribute = useCallback(
|
||||
@ -78,7 +86,7 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
|
||||
(nextSource: DataSource): void => {
|
||||
const newOperators = getOperatorsBySourceAndPanelType({
|
||||
dataSource: nextSource,
|
||||
panelType,
|
||||
panelType: panelType || PANEL_TYPES.TIME_SERIES,
|
||||
});
|
||||
|
||||
const entries = Object.entries(
|
||||
@ -121,33 +129,22 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
|
||||
[query.dataSource],
|
||||
);
|
||||
|
||||
const isTracePanelType = useMemo(() => panelType === PANEL_TYPES.TRACE, [
|
||||
panelType,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialDataSource && dataSource !== initialDataSource) return;
|
||||
|
||||
const initialOperators = getOperatorsBySourceAndPanelType({
|
||||
dataSource,
|
||||
panelType,
|
||||
panelType: panelType || PANEL_TYPES.TIME_SERIES,
|
||||
});
|
||||
|
||||
if (JSON.stringify(operators) === JSON.stringify(initialOperators)) return;
|
||||
|
||||
setOperators(initialOperators);
|
||||
|
||||
const isCurrentOperatorAvailableInList = initialOperators
|
||||
.map((operator) => operator.value)
|
||||
.includes(aggregateOperator);
|
||||
|
||||
if (!isCurrentOperatorAvailableInList) {
|
||||
handleChangeOperator(initialOperators[0].value);
|
||||
}
|
||||
}, [
|
||||
dataSource,
|
||||
initialDataSource,
|
||||
panelType,
|
||||
operators,
|
||||
aggregateOperator,
|
||||
handleChangeOperator,
|
||||
]);
|
||||
}, [dataSource, initialDataSource, panelType, operators]);
|
||||
|
||||
useEffect(() => {
|
||||
const additionalFilters = getNewListOfAdditionalFilters(dataSource);
|
||||
@ -156,6 +153,7 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
|
||||
}, [dataSource, aggregateOperator, getNewListOfAdditionalFilters]);
|
||||
|
||||
return {
|
||||
isTracePanelType,
|
||||
isMetricsDataSource,
|
||||
operators,
|
||||
listOfAdditionalFilters,
|
||||
|
@ -5,11 +5,9 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { useGetCompositeQueryParam } from './useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from './useQueryBuilder';
|
||||
|
||||
type UseShareBuilderUrlParams = { defaultValue: Query };
|
||||
export type UseShareBuilderUrlParams = { defaultValue: Query };
|
||||
|
||||
export const useShareBuilderUrl = ({
|
||||
defaultValue,
|
||||
}: UseShareBuilderUrlParams): void => {
|
||||
export const useShareBuilderUrl = (defaultQuery: Query): void => {
|
||||
const { redirectWithQueryBuilderData, resetStagedQuery } = useQueryBuilder();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
@ -17,9 +15,9 @@ export const useShareBuilderUrl = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (!compositeQuery) {
|
||||
redirectWithQueryBuilderData(defaultValue);
|
||||
redirectWithQueryBuilderData(defaultQuery);
|
||||
}
|
||||
}, [defaultValue, urlQuery, redirectWithQueryBuilderData, compositeQuery]);
|
||||
}, [defaultQuery, urlQuery, redirectWithQueryBuilderData, compositeQuery]);
|
||||
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
|
37
frontend/src/hooks/queryBuilder/useStepInterval.ts
Normal file
37
frontend/src/hooks/queryBuilder/useStepInterval.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import getStep from 'lib/getStep';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
export const updateStepInterval = (
|
||||
query: Widgets['query'],
|
||||
maxTime: number,
|
||||
minTime: number,
|
||||
): Widgets['query'] => {
|
||||
const stepInterval = getStep({
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
inputFormat: 'ns',
|
||||
});
|
||||
|
||||
return {
|
||||
...query,
|
||||
builder: {
|
||||
...query?.builder,
|
||||
queryData:
|
||||
query?.builder?.queryData?.map((item) => ({
|
||||
...item,
|
||||
stepInterval,
|
||||
})) || [],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const useStepInterval = (query: Widgets['query']): Widgets['query'] => {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
return updateStepInterval(query, maxTime, minTime);
|
||||
};
|
3
frontend/src/hooks/queryPagination/config.ts
Normal file
3
frontend/src/hooks/queryPagination/config.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const URL_PAGINATION = 'pagination';
|
||||
|
||||
export const DEFAULT_PER_PAGE_OPTIONS: number[] = [25, 50, 100, 200];
|
2
frontend/src/hooks/queryPagination/index.ts
Normal file
2
frontend/src/hooks/queryPagination/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './config';
|
||||
export * from './types';
|
4
frontend/src/hooks/queryPagination/types.ts
Normal file
4
frontend/src/hooks/queryPagination/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface Pagination {
|
||||
offset: number;
|
||||
limit: number;
|
||||
}
|
88
frontend/src/hooks/queryPagination/useQueryPagination.ts
Normal file
88
frontend/src/hooks/queryPagination/useQueryPagination.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { ControlsProps } from 'container/Controls';
|
||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
import { DEFAULT_PER_PAGE_OPTIONS, URL_PAGINATION } from './config';
|
||||
import { Pagination } from './types';
|
||||
import {
|
||||
checkIsValidPaginationData,
|
||||
getDefaultPaginationConfig,
|
||||
} from './utils';
|
||||
|
||||
const useQueryPagination = (
|
||||
totalCount: number,
|
||||
perPageOptions: number[] = DEFAULT_PER_PAGE_OPTIONS,
|
||||
): UseQueryPagination => {
|
||||
const defaultPaginationConfig = useMemo(
|
||||
() => getDefaultPaginationConfig(perPageOptions),
|
||||
[perPageOptions],
|
||||
);
|
||||
|
||||
const {
|
||||
query: paginationQuery,
|
||||
queryData: paginationQueryData,
|
||||
redirectWithQuery: redirectWithCurrentPagination,
|
||||
} = useUrlQueryData<Pagination>(URL_PAGINATION);
|
||||
|
||||
const handleCountItemsPerPageChange = useCallback(
|
||||
(newLimit: Pagination['limit']) => {
|
||||
redirectWithCurrentPagination({
|
||||
...paginationQueryData,
|
||||
limit: newLimit,
|
||||
});
|
||||
},
|
||||
[paginationQueryData, redirectWithCurrentPagination],
|
||||
);
|
||||
|
||||
const handleNavigatePrevious = useCallback(() => {
|
||||
const previousOffset = paginationQueryData.offset - paginationQueryData.limit;
|
||||
|
||||
redirectWithCurrentPagination({
|
||||
...paginationQueryData,
|
||||
offset: previousOffset > 0 ? previousOffset : 0,
|
||||
});
|
||||
}, [paginationQueryData, redirectWithCurrentPagination]);
|
||||
|
||||
const handleNavigateNext = useCallback(() => {
|
||||
redirectWithCurrentPagination({
|
||||
...paginationQueryData,
|
||||
offset:
|
||||
paginationQueryData.limit === totalCount
|
||||
? paginationQueryData.offset + paginationQueryData.limit
|
||||
: paginationQueryData.offset,
|
||||
});
|
||||
}, [totalCount, paginationQueryData, redirectWithCurrentPagination]);
|
||||
|
||||
useEffect(() => {
|
||||
const isValidPaginationData = checkIsValidPaginationData(
|
||||
paginationQueryData || defaultPaginationConfig,
|
||||
perPageOptions,
|
||||
);
|
||||
|
||||
if (paginationQuery && isValidPaginationData) return;
|
||||
|
||||
redirectWithCurrentPagination(defaultPaginationConfig);
|
||||
}, [
|
||||
defaultPaginationConfig,
|
||||
perPageOptions,
|
||||
paginationQuery,
|
||||
paginationQueryData,
|
||||
redirectWithCurrentPagination,
|
||||
]);
|
||||
|
||||
return {
|
||||
pagination: paginationQueryData || defaultPaginationConfig,
|
||||
handleCountItemsPerPageChange,
|
||||
handleNavigatePrevious,
|
||||
handleNavigateNext,
|
||||
};
|
||||
};
|
||||
|
||||
type UseQueryPagination = Pick<
|
||||
ControlsProps,
|
||||
| 'handleCountItemsPerPageChange'
|
||||
| 'handleNavigateNext'
|
||||
| 'handleNavigatePrevious'
|
||||
> & { pagination: Pagination };
|
||||
|
||||
export default useQueryPagination;
|
20
frontend/src/hooks/queryPagination/utils.ts
Normal file
20
frontend/src/hooks/queryPagination/utils.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { DEFAULT_PER_PAGE_OPTIONS } from './config';
|
||||
import { Pagination } from './types';
|
||||
|
||||
export const checkIsValidPaginationData = (
|
||||
{ limit, offset }: Pagination,
|
||||
perPageOptions: number[],
|
||||
): boolean =>
|
||||
Boolean(
|
||||
Number.isInteger(limit) &&
|
||||
limit > 0 &&
|
||||
offset >= 0 &&
|
||||
perPageOptions.find((option) => option === limit),
|
||||
);
|
||||
|
||||
export const getDefaultPaginationConfig = (
|
||||
perPageOptions = DEFAULT_PER_PAGE_OPTIONS,
|
||||
): Pagination => ({
|
||||
offset: 0,
|
||||
limit: perPageOptions[0],
|
||||
});
|
44
frontend/src/hooks/useUrlQueryData.ts
Normal file
44
frontend/src/hooks/useUrlQueryData.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
|
||||
const useUrlQueryData = <T>(
|
||||
queryKey: string,
|
||||
defaultData?: T,
|
||||
): UseUrlQueryData<T> => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const urlQuery = useUrlQuery();
|
||||
|
||||
const query = useMemo(() => urlQuery.get(queryKey), [queryKey, urlQuery]);
|
||||
|
||||
const queryData: T = useMemo(() => (query ? JSON.parse(query) : defaultData), [
|
||||
query,
|
||||
defaultData,
|
||||
]);
|
||||
|
||||
const redirectWithQuery = useCallback(
|
||||
(newQueryData: T): void => {
|
||||
const newQuery = JSON.stringify(newQueryData);
|
||||
|
||||
urlQuery.set(queryKey, newQuery);
|
||||
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
||||
history.push(generatedUrl);
|
||||
},
|
||||
[history, location, urlQuery, queryKey],
|
||||
);
|
||||
|
||||
return {
|
||||
query,
|
||||
queryData,
|
||||
redirectWithQuery,
|
||||
};
|
||||
};
|
||||
|
||||
interface UseUrlQueryData<T> {
|
||||
query: string | null;
|
||||
queryData: T;
|
||||
redirectWithQuery: (newQueryData: T) => void;
|
||||
}
|
||||
|
||||
export default useUrlQueryData;
|
@ -39,7 +39,12 @@ describe('lib/getStep', () => {
|
||||
const startUnix = start.valueOf();
|
||||
const endUnix = end.valueOf();
|
||||
|
||||
const expectedStepSize = Math.floor(end.diff(start, 's') / MaxDataPoints);
|
||||
let expectedStepSize = Math.max(
|
||||
Math.floor(end.diff(start, 's') / MaxDataPoints),
|
||||
DefaultStepSize,
|
||||
);
|
||||
|
||||
expectedStepSize -= expectedStepSize % 60;
|
||||
|
||||
expect(
|
||||
getStep({
|
||||
|
@ -7,7 +7,7 @@ export const getDashboardVariables = (): Record<string, unknown> => {
|
||||
globalTime,
|
||||
dashboards: { dashboards },
|
||||
} = store.getState();
|
||||
const [selectedDashboard] = dashboards;
|
||||
const [selectedDashboard] = dashboards || [];
|
||||
const {
|
||||
data: { variables = {} },
|
||||
} = selectedDashboard;
|
||||
|
137
frontend/src/lib/getStep.test.ts
Normal file
137
frontend/src/lib/getStep.test.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import getStep, { DefaultStepSize, MaxDataPoints } from './getStep';
|
||||
|
||||
describe('get dynamic step size', () => {
|
||||
test('should return default step size if diffSec is less than MaxDataPoints', () => {
|
||||
const start = dayjs().subtract(1, 'minute').valueOf();
|
||||
const end = dayjs().valueOf();
|
||||
|
||||
const step = getStep({
|
||||
start,
|
||||
end,
|
||||
inputFormat: 'ms',
|
||||
});
|
||||
|
||||
expect(step).toBe(DefaultStepSize);
|
||||
});
|
||||
|
||||
test('should return appropriate step size if diffSec is more than MaxDataPoints', () => {
|
||||
const start = dayjs().subtract(4, 'hour').valueOf();
|
||||
const end = dayjs().valueOf();
|
||||
|
||||
const step = getStep({
|
||||
start,
|
||||
end,
|
||||
inputFormat: 'ms',
|
||||
});
|
||||
|
||||
// the expected step size should be no less than DefaultStepSize
|
||||
const diffSec = Math.abs(dayjs(end).diff(dayjs(start), 's'));
|
||||
const expectedStep = Math.max(
|
||||
Math.floor(diffSec / MaxDataPoints),
|
||||
DefaultStepSize,
|
||||
);
|
||||
|
||||
expect(step).toBe(expectedStep);
|
||||
});
|
||||
|
||||
test('should correctly handle different input formats', () => {
|
||||
const endSec = dayjs().unix();
|
||||
const startSec = endSec - 4 * 3600; // 4 hours earlier
|
||||
|
||||
const stepSec = getStep({
|
||||
start: startSec,
|
||||
end: endSec,
|
||||
inputFormat: 's',
|
||||
});
|
||||
|
||||
const diffSec = Math.abs(dayjs.unix(endSec).diff(dayjs.unix(startSec), 's'));
|
||||
const expectedStep = Math.max(
|
||||
Math.floor(diffSec / MaxDataPoints),
|
||||
DefaultStepSize,
|
||||
);
|
||||
|
||||
expect(stepSec).toBe(expectedStep);
|
||||
|
||||
const startNs = startSec * 1e9; // convert to nanoseconds
|
||||
const endNs = endSec * 1e9; // convert to nanoseconds
|
||||
|
||||
const stepNs = getStep({
|
||||
start: startNs,
|
||||
end: endNs,
|
||||
inputFormat: 'ns',
|
||||
});
|
||||
|
||||
expect(stepNs).toBe(expectedStep); // Expect the same result as 's' inputFormat
|
||||
});
|
||||
|
||||
test('should throw an error for invalid input format', () => {
|
||||
const start = dayjs().valueOf();
|
||||
const end = dayjs().valueOf();
|
||||
|
||||
expect(() => {
|
||||
getStep({
|
||||
start,
|
||||
end,
|
||||
inputFormat: 'invalid' as never,
|
||||
});
|
||||
}).toThrow('invalid format');
|
||||
});
|
||||
|
||||
test('should return DefaultStepSize when start and end are the same', () => {
|
||||
const start = dayjs().valueOf();
|
||||
const end = start; // same as start
|
||||
|
||||
const step = getStep({
|
||||
start,
|
||||
end,
|
||||
inputFormat: 'ms',
|
||||
});
|
||||
|
||||
expect(step).toBe(DefaultStepSize);
|
||||
});
|
||||
|
||||
test('should return DefaultStepSize if diffSec is exactly MaxDataPoints', () => {
|
||||
const endMs = dayjs().valueOf();
|
||||
const startMs = endMs - MaxDataPoints * 1000; // exactly MaxDataPoints seconds earlier
|
||||
|
||||
const step = getStep({
|
||||
start: startMs,
|
||||
end: endMs,
|
||||
inputFormat: 'ms',
|
||||
});
|
||||
|
||||
expect(step).toBe(DefaultStepSize); // since calculated step size is less than DefaultStepSize, it should return DefaultStepSize
|
||||
});
|
||||
|
||||
test('should return DefaultStepSize for future dates less than (MaxDataPoints * DefaultStepSize) seconds ahead', () => {
|
||||
const start = dayjs().valueOf();
|
||||
const end = start + MaxDataPoints * DefaultStepSize * 1000 - 1; // just one millisecond less than (MaxDataPoints * DefaultStepSize) seconds ahead
|
||||
|
||||
const step = getStep({
|
||||
start,
|
||||
end,
|
||||
inputFormat: 'ms',
|
||||
});
|
||||
|
||||
expect(step).toBe(DefaultStepSize);
|
||||
});
|
||||
|
||||
test('should handle string inputs correctly for a time range greater than (MaxDataPoints * DefaultStepSize) seconds', () => {
|
||||
const endMs = dayjs().valueOf();
|
||||
const startMs = endMs - (MaxDataPoints * DefaultStepSize * 1000 + 1); // one millisecond more than (MaxDataPoints * DefaultStepSize) seconds earlier
|
||||
|
||||
const step = getStep({
|
||||
start: startMs.toString(),
|
||||
end: endMs.toString(),
|
||||
inputFormat: 'ms',
|
||||
});
|
||||
|
||||
const diffSec = Math.abs(
|
||||
dayjs(Number(endMs)).diff(dayjs(Number(startMs)), 's'),
|
||||
);
|
||||
|
||||
expect(step).toBe(Math.floor(diffSec / MaxDataPoints));
|
||||
});
|
||||
});
|
@ -30,7 +30,7 @@ const convertToMs = (
|
||||
};
|
||||
|
||||
export const DefaultStepSize = 60;
|
||||
export const MaxDataPoints = 200;
|
||||
export const MaxDataPoints = 300;
|
||||
|
||||
/**
|
||||
* Returns relevant step size based on given start and end date.
|
||||
@ -40,7 +40,13 @@ const getStep = ({ start, end, inputFormat = 'ms' }: GetStepInput): number => {
|
||||
const endDate = dayjs(convertToMs(Number(end), inputFormat));
|
||||
const diffSec = Math.abs(endDate.diff(startDate, 's'));
|
||||
|
||||
return Math.max(Math.floor(diffSec / MaxDataPoints), DefaultStepSize);
|
||||
let result =
|
||||
Math.max(Math.floor(diffSec / MaxDataPoints), DefaultStepSize) ||
|
||||
DefaultStepSize;
|
||||
|
||||
result -= result % 60;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default getStep;
|
||||
|
@ -35,5 +35,9 @@ export const convertNewDataToOld = (
|
||||
});
|
||||
const oldResultType = resultType;
|
||||
|
||||
return { data: { result: oldResult, resultType: oldResultType } };
|
||||
// TODO: fix it later for using only v3 version of api
|
||||
|
||||
return {
|
||||
data: { result: oldResult, resultType: oldResultType, newResult: newData },
|
||||
};
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user