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
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.21.0
|
image: signoz/query-service:0.22.0
|
||||||
command: ["-config=/root/config/prometheus.yml"]
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
# ports:
|
# ports:
|
||||||
# - "6060:6060" # pprof port
|
# - "6060:6060" # pprof port
|
||||||
@ -166,7 +166,7 @@ services:
|
|||||||
<<: *clickhouse-depend
|
<<: *clickhouse-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.21.0
|
image: signoz/frontend:0.22.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@ -179,7 +179,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
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"]
|
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||||
user: root # required for reading docker container logs
|
user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
@ -208,7 +208,7 @@ services:
|
|||||||
<<: *clickhouse-depend
|
<<: *clickhouse-depend
|
||||||
|
|
||||||
otel-collector-metrics:
|
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"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
@ -233,7 +233,7 @@ services:
|
|||||||
max-file: "3"
|
max-file: "3"
|
||||||
|
|
||||||
load-hotrod:
|
load-hotrod:
|
||||||
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
|
image: "signoz/locust:1.2.3"
|
||||||
hostname: load-hotrod
|
hostname: load-hotrod
|
||||||
environment:
|
environment:
|
||||||
ATTACKED_HOST: http://hotrod:8080
|
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`
|
# 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:
|
otel-collector:
|
||||||
container_name: 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"]
|
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||||
# user: root # required for reading docker container logs
|
# user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
@ -67,7 +67,7 @@ services:
|
|||||||
|
|
||||||
otel-collector-metrics:
|
otel-collector-metrics:
|
||||||
container_name: 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"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
@ -93,7 +93,7 @@ services:
|
|||||||
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
|
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
|
||||||
|
|
||||||
load-hotrod:
|
load-hotrod:
|
||||||
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
|
image: "signoz/locust:1.2.3"
|
||||||
container_name: load-hotrod
|
container_name: load-hotrod
|
||||||
hostname: load-hotrod
|
hostname: load-hotrod
|
||||||
environment:
|
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`
|
# 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:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.21.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.22.0}
|
||||||
container_name: query-service
|
container_name: query-service
|
||||||
command: ["-config=/root/config/prometheus.yml"]
|
command: ["-config=/root/config/prometheus.yml"]
|
||||||
# ports:
|
# ports:
|
||||||
@ -181,7 +181,7 @@ services:
|
|||||||
<<: *clickhouse-depend
|
<<: *clickhouse-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.21.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.22.0}
|
||||||
container_name: frontend
|
container_name: frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -193,7 +193,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
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"]
|
command: ["--config=/etc/otel-collector-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||||
user: root # required for reading docker container logs
|
user: root # required for reading docker container logs
|
||||||
volumes:
|
volumes:
|
||||||
@ -219,7 +219,7 @@ services:
|
|||||||
<<: *clickhouse-depend
|
<<: *clickhouse-depend
|
||||||
|
|
||||||
otel-collector-metrics:
|
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"]
|
command: ["--config=/etc/otel-collector-metrics-config.yaml", "--feature-gates=-pkg.translator.prometheus.NormalizeName"]
|
||||||
volumes:
|
volumes:
|
||||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||||
@ -243,7 +243,7 @@ services:
|
|||||||
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
|
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
|
||||||
|
|
||||||
load-hotrod:
|
load-hotrod:
|
||||||
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
|
image: "signoz/locust:1.2.3"
|
||||||
container_name: load-hotrod
|
container_name: load-hotrod
|
||||||
hostname: load-hotrod
|
hostname: load-hotrod
|
||||||
environment:
|
environment:
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
type APIHandlerOptions struct {
|
type APIHandlerOptions struct {
|
||||||
DataConnector interfaces.DataConnector
|
DataConnector interfaces.DataConnector
|
||||||
|
SkipConfig *basemodel.SkipConfig
|
||||||
AppDao dao.ModelDao
|
AppDao dao.ModelDao
|
||||||
RulesManager *rules.Manager
|
RulesManager *rules.Manager
|
||||||
FeatureFlags baseint.FeatureLookup
|
FeatureFlags baseint.FeatureLookup
|
||||||
@ -32,6 +33,7 @@ func NewAPIHandler(opts APIHandlerOptions) (*APIHandler, error) {
|
|||||||
|
|
||||||
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
baseHandler, err := baseapp.NewAPIHandler(baseapp.APIHandlerOpts{
|
||||||
Reader: opts.DataConnector,
|
Reader: opts.DataConnector,
|
||||||
|
SkipConfig: opts.SkipConfig,
|
||||||
AppDao: opts.AppDao,
|
AppDao: opts.AppDao,
|
||||||
RuleManager: opts.RulesManager,
|
RuleManager: opts.RulesManager,
|
||||||
FeatureFlags: opts.FeatureFlags})
|
FeatureFlags: opts.FeatureFlags})
|
||||||
|
@ -49,9 +49,10 @@ import (
|
|||||||
const AppDbEngine = "sqlite"
|
const AppDbEngine = "sqlite"
|
||||||
|
|
||||||
type ServerOptions struct {
|
type ServerOptions struct {
|
||||||
PromConfigPath string
|
PromConfigPath string
|
||||||
HTTPHostPort string
|
SkipTopLvlOpsPath string
|
||||||
PrivateHostPort string
|
HTTPHostPort string
|
||||||
|
PrivateHostPort string
|
||||||
// alert specific params
|
// alert specific params
|
||||||
DisableRules bool
|
DisableRules bool
|
||||||
RuleRepoURL string
|
RuleRepoURL string
|
||||||
@ -119,7 +120,15 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
go qb.Start(readerReady)
|
go qb.Start(readerReady)
|
||||||
reader = qb
|
reader = qb
|
||||||
} else {
|
} 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
|
<-readerReady
|
||||||
@ -160,6 +169,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
|
|
||||||
apiOpts := api.APIHandlerOptions{
|
apiOpts := api.APIHandlerOptions{
|
||||||
DataConnector: reader,
|
DataConnector: reader,
|
||||||
|
SkipConfig: skipConfig,
|
||||||
AppDao: modelDao,
|
AppDao: modelDao,
|
||||||
RulesManager: rm,
|
RulesManager: rm,
|
||||||
FeatureFlags: lm,
|
FeatureFlags: lm,
|
||||||
|
@ -74,7 +74,7 @@ func initZapLog(enableQueryServiceLogOTLPExport bool) *zap.Logger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var promConfigPath string
|
var promConfigPath, skipTopLvlOpsPath string
|
||||||
|
|
||||||
// disables rule execution but allows change to the rule definition
|
// disables rule execution but allows change to the rule definition
|
||||||
var disableRules bool
|
var disableRules bool
|
||||||
@ -85,6 +85,7 @@ func main() {
|
|||||||
var enableQueryServiceLogOTLPExport bool
|
var enableQueryServiceLogOTLPExport bool
|
||||||
|
|
||||||
flag.StringVar(&promConfigPath, "config", "./config/prometheus.yml", "(prometheus config to read metrics)")
|
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.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.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)")
|
flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)")
|
||||||
@ -98,11 +99,12 @@ func main() {
|
|||||||
version.PrintVersion()
|
version.PrintVersion()
|
||||||
|
|
||||||
serverOptions := &app.ServerOptions{
|
serverOptions := &app.ServerOptions{
|
||||||
HTTPHostPort: baseconst.HTTPHostPort,
|
HTTPHostPort: baseconst.HTTPHostPort,
|
||||||
PromConfigPath: promConfigPath,
|
PromConfigPath: promConfigPath,
|
||||||
PrivateHostPort: baseconst.PrivateHostPort,
|
SkipTopLvlOpsPath: skipTopLvlOpsPath,
|
||||||
DisableRules: disableRules,
|
PrivateHostPort: baseconst.PrivateHostPort,
|
||||||
RuleRepoURL: ruleRepoURL,
|
DisableRules: disableRules,
|
||||||
|
RuleRepoURL: ruleRepoURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the jwt secret key
|
// Read the jwt secret key
|
||||||
|
@ -181,6 +181,9 @@ function Graph({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
position: 'custom',
|
position: 'custom',
|
||||||
|
itemSort(item1, item2) {
|
||||||
|
return item2.parsed.y - item1.parsed.y;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
[dragSelectPluginId]: createDragSelectPluginOptions(
|
[dragSelectPluginId]: createDragSelectPluginOptions(
|
||||||
!!onDragSelect,
|
!!onDragSelect,
|
||||||
|
@ -1,14 +1,20 @@
|
|||||||
import { Table } from 'antd';
|
import { Table } from 'antd';
|
||||||
import type { TableProps } from 'antd/es/table';
|
import type { TableProps } from 'antd/es/table';
|
||||||
import { ColumnsType } from 'antd/lib/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 { ResizeCallbackData } from 'react-resizable';
|
||||||
|
|
||||||
import ResizableHeader from './ResizableHeader';
|
import ResizableHeader from './ResizableHeader';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function ResizeTable({ columns, ...restprops }: TableProps<any>): JSX.Element {
|
function ResizeTable({ columns, ...restprops }: TableProps<any>): JSX.Element {
|
||||||
const [columnsData, setColumns] = useState<ColumnsType>(columns || []);
|
const [columnsData, setColumns] = useState<ColumnsType>([]);
|
||||||
|
|
||||||
const handleResize = useCallback(
|
const handleResize = useCallback(
|
||||||
(index: number) => (
|
(index: number) => (
|
||||||
@ -37,6 +43,12 @@ function ResizeTable({ columns, ...restprops }: TableProps<any>): JSX.Element {
|
|||||||
[columnsData, handleResize],
|
[columnsData, handleResize],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (columns) {
|
||||||
|
setColumns(columns);
|
||||||
|
}
|
||||||
|
}, [columns]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
@ -12,4 +12,8 @@ export enum QueryParams {
|
|||||||
aggregationOption = 'aggregationOption',
|
aggregationOption = 'aggregationOption',
|
||||||
entity = 'entity',
|
entity = 'entity',
|
||||||
resourceAttributes = 'resourceAttribute',
|
resourceAttributes = 'resourceAttribute',
|
||||||
|
graphType = 'graphType',
|
||||||
|
widgetId = 'widgetId',
|
||||||
|
order = 'order',
|
||||||
|
q = 'q',
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ const initialQueryBuilderFormValues: IBuilderQuery = {
|
|||||||
}),
|
}),
|
||||||
disabled: false,
|
disabled: false,
|
||||||
having: [],
|
having: [],
|
||||||
stepInterval: 30,
|
stepInterval: 60,
|
||||||
limit: null,
|
limit: null,
|
||||||
orderBy: [],
|
orderBy: [],
|
||||||
groupBy: [],
|
groupBy: [],
|
||||||
@ -232,6 +232,7 @@ export const PANEL_TYPES: Record<PanelTypeKeys, GRAPH_TYPES> = {
|
|||||||
VALUE: 'value',
|
VALUE: 'value',
|
||||||
TABLE: 'table',
|
TABLE: 'table',
|
||||||
LIST: 'list',
|
LIST: 'list',
|
||||||
|
TRACE: 'trace',
|
||||||
EMPTY_WIDGET: 'EMPTY_WIDGET',
|
EMPTY_WIDGET: 'EMPTY_WIDGET',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,33 +1,30 @@
|
|||||||
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
import { LeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
import { Button, Select } from 'antd';
|
import { Button, Select } from 'antd';
|
||||||
|
import { DEFAULT_PER_PAGE_OPTIONS, Pagination } from 'hooks/queryPagination';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
|
|
||||||
import { defaultSelectStyle, ITEMS_PER_PAGE_OPTIONS } from './config';
|
import { defaultSelectStyle } from './config';
|
||||||
import { Container } from './styles';
|
import { Container } from './styles';
|
||||||
|
|
||||||
interface ControlsProps {
|
function Controls({
|
||||||
count: number;
|
offset = 0,
|
||||||
countPerPage: number;
|
perPageOptions = DEFAULT_PER_PAGE_OPTIONS,
|
||||||
isLoading: boolean;
|
isLoading,
|
||||||
handleNavigatePrevious: () => void;
|
totalCount,
|
||||||
handleNavigateNext: () => void;
|
countPerPage,
|
||||||
handleCountItemsPerPageChange: (e: number) => void;
|
handleNavigatePrevious,
|
||||||
}
|
handleNavigateNext,
|
||||||
|
handleCountItemsPerPageChange,
|
||||||
function Controls(props: ControlsProps): JSX.Element | null {
|
}: ControlsProps): JSX.Element | null {
|
||||||
const {
|
|
||||||
count,
|
|
||||||
isLoading,
|
|
||||||
countPerPage,
|
|
||||||
handleNavigatePrevious,
|
|
||||||
handleNavigateNext,
|
|
||||||
handleCountItemsPerPageChange,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const isNextAndPreviousDisabled = useMemo(
|
const isNextAndPreviousDisabled = useMemo(
|
||||||
() => isLoading || countPerPage === 0 || count === 0 || count < countPerPage,
|
() => isLoading || countPerPage < 0 || totalCount === 0,
|
||||||
[isLoading, countPerPage, count],
|
[isLoading, countPerPage, totalCount],
|
||||||
);
|
);
|
||||||
|
const isPreviousDisabled = useMemo(() => offset <= 0, [offset]);
|
||||||
|
const isNextDisabled = useMemo(() => totalCount < countPerPage, [
|
||||||
|
countPerPage,
|
||||||
|
totalCount,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
@ -35,7 +32,7 @@ function Controls(props: ControlsProps): JSX.Element | null {
|
|||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
size="small"
|
size="small"
|
||||||
type="link"
|
type="link"
|
||||||
disabled={isNextAndPreviousDisabled}
|
disabled={isPreviousDisabled || isNextAndPreviousDisabled}
|
||||||
onClick={handleNavigatePrevious}
|
onClick={handleNavigatePrevious}
|
||||||
>
|
>
|
||||||
<LeftOutlined /> Previous
|
<LeftOutlined /> Previous
|
||||||
@ -44,18 +41,18 @@ function Controls(props: ControlsProps): JSX.Element | null {
|
|||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
size="small"
|
size="small"
|
||||||
type="link"
|
type="link"
|
||||||
disabled={isNextAndPreviousDisabled}
|
disabled={isNextDisabled || isNextAndPreviousDisabled}
|
||||||
onClick={handleNavigateNext}
|
onClick={handleNavigateNext}
|
||||||
>
|
>
|
||||||
Next <RightOutlined />
|
Next <RightOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
<Select
|
<Select<Pagination['limit']>
|
||||||
style={defaultSelectStyle}
|
style={defaultSelectStyle}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
value={countPerPage}
|
value={countPerPage}
|
||||||
onChange={handleCountItemsPerPageChange}
|
onChange={handleCountItemsPerPageChange}
|
||||||
>
|
>
|
||||||
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
|
{perPageOptions.map((count) => (
|
||||||
<Select.Option
|
<Select.Option
|
||||||
key={count}
|
key={count}
|
||||||
value={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);
|
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 { Row } from 'antd';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
|
|
||||||
|
import { getOptionList } from './config';
|
||||||
import { AlertTypeCard, SelectTypeContainer } from './styles';
|
import { AlertTypeCard, SelectTypeContainer } from './styles';
|
||||||
|
import { OptionType } from './types';
|
||||||
interface OptionType {
|
|
||||||
title: string;
|
|
||||||
selection: AlertTypes;
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||||
const { t } = useTranslation(['alerts']);
|
const { t } = useTranslation(['alerts']);
|
||||||
|
|
||||||
const renderOptions = (): JSX.Element => {
|
const optionList = getOptionList(t);
|
||||||
const optionList: OptionType[] = [
|
|
||||||
{
|
const renderOptions = useMemo(
|
||||||
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 (
|
|
||||||
<>
|
<>
|
||||||
{optionList.map((o: OptionType) => (
|
{optionList.map((option: OptionType) => (
|
||||||
<AlertTypeCard
|
<AlertTypeCard
|
||||||
key={o.selection}
|
key={option.selection}
|
||||||
title={o.title}
|
title={option.title}
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
onSelect(o.selection);
|
onSelect(option.selection);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{o.description}
|
{option.description}
|
||||||
</AlertTypeCard>
|
</AlertTypeCard>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
),
|
||||||
};
|
[onSelect, optionList],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectTypeContainer>
|
<SelectTypeContainer>
|
||||||
<h3> {t('choose_alert_type')} </h3>
|
<h3> {t('choose_alert_type')} </h3>
|
||||||
<Row>{renderOptions()}</Row>
|
<Row>{renderOptions}</Row>
|
||||||
</SelectTypeContainer>
|
</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,
|
disabled: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
queryType: EQueryType.CLICKHOUSE,
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
panelType: PANEL_TYPES.TIME_SERIES,
|
panelType: PANEL_TYPES.TIME_SERIES,
|
||||||
},
|
},
|
||||||
op: defaultCompareOp,
|
op: defaultCompareOp,
|
||||||
@ -67,7 +67,7 @@ export const logAlertDefaults: AlertDef = {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
queryType: EQueryType.CLICKHOUSE,
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
panelType: PANEL_TYPES.TIME_SERIES,
|
panelType: PANEL_TYPES.TIME_SERIES,
|
||||||
},
|
},
|
||||||
op: defaultCompareOp,
|
op: defaultCompareOp,
|
||||||
@ -97,7 +97,7 @@ export const traceAlertDefaults: AlertDef = {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
queryType: EQueryType.CLICKHOUSE,
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
panelType: PANEL_TYPES.TIME_SERIES,
|
panelType: PANEL_TYPES.TIME_SERIES,
|
||||||
},
|
},
|
||||||
op: defaultCompareOp,
|
op: defaultCompareOp,
|
||||||
@ -127,7 +127,7 @@ export const exceptionAlertDefaults: AlertDef = {
|
|||||||
disabled: false,
|
disabled: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
queryType: EQueryType.CLICKHOUSE,
|
queryType: EQueryType.QUERY_BUILDER,
|
||||||
panelType: PANEL_TYPES.TIME_SERIES,
|
panelType: PANEL_TYPES.TIME_SERIES,
|
||||||
},
|
},
|
||||||
op: defaultCompareOp,
|
op: defaultCompareOp,
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { Form, Row } from 'antd';
|
import { Form, Row } from 'antd';
|
||||||
import FormAlertRules from 'container/FormAlertRules';
|
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 { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
import { AlertDef } from 'types/api/alerts/def';
|
import { AlertDef } from 'types/api/alerts/def';
|
||||||
|
|
||||||
|
import { ALERT_TYPE_VS_SOURCE_MAPPING } from './config';
|
||||||
import {
|
import {
|
||||||
alertDefaults,
|
alertDefaults,
|
||||||
exceptionAlertDefaults,
|
exceptionAlertDefaults,
|
||||||
@ -17,6 +19,9 @@ function CreateRules(): JSX.Element {
|
|||||||
const [alertType, setAlertType] = useState<AlertTypes>(
|
const [alertType, setAlertType] = useState<AlertTypes>(
|
||||||
AlertTypes.METRICS_BASED_ALERT,
|
AlertTypes.METRICS_BASED_ALERT,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const compositeQuery = useGetCompositeQueryParam();
|
||||||
|
|
||||||
const [formInstance] = Form.useForm();
|
const [formInstance] = Form.useForm();
|
||||||
|
|
||||||
const onSelectType = (typ: AlertTypes): void => {
|
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) {
|
if (!initValues) {
|
||||||
return (
|
return (
|
||||||
<Row wrap={false}>
|
<Row wrap={false}>
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
import { Button, Typography } from 'antd';
|
import { Button, Typography } from 'antd';
|
||||||
import createDashboard from 'api/dashboard/create';
|
import createDashboard from 'api/dashboard/create';
|
||||||
import getAll from 'api/dashboard/getAll';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMutation, useQuery } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
|
|
||||||
import { ExportPanelProps } from '.';
|
import { ExportPanelProps } from '.';
|
||||||
import {
|
import {
|
||||||
@ -18,7 +17,7 @@ import {
|
|||||||
} from './styles';
|
} from './styles';
|
||||||
import { getSelectOptions } from './utils';
|
import { getSelectOptions } from './utils';
|
||||||
|
|
||||||
function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
|
function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
const { t } = useTranslation(['dashboard']);
|
const { t } = useTranslation(['dashboard']);
|
||||||
|
|
||||||
@ -26,16 +25,18 @@ function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, isLoading, refetch } = useQuery({
|
const {
|
||||||
queryFn: getAll,
|
data,
|
||||||
queryKey: REACT_QUERY_KEY.GET_ALL_DASHBOARDS,
|
isLoading: isAllDashboardsLoading,
|
||||||
});
|
refetch,
|
||||||
|
} = useGetAllDashboard();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutate: createNewDashboard,
|
mutate: createNewDashboard,
|
||||||
isLoading: createDashboardLoading,
|
isLoading: createDashboardLoading,
|
||||||
} = useMutation(createDashboard, {
|
} = useMutation(createDashboard, {
|
||||||
onSuccess: () => {
|
onSuccess: (data) => {
|
||||||
|
onExport(data?.payload || null);
|
||||||
refetch();
|
refetch();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
@ -73,6 +74,14 @@ function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
|
|||||||
});
|
});
|
||||||
}, [t, createNewDashboard]);
|
}, [t, createNewDashboard]);
|
||||||
|
|
||||||
|
const isDashboardLoading = isAllDashboardsLoading || createDashboardLoading;
|
||||||
|
|
||||||
|
const isDisabled =
|
||||||
|
isAllDashboardsLoading ||
|
||||||
|
!options?.length ||
|
||||||
|
!selectedDashboardId ||
|
||||||
|
isLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper direction="vertical">
|
<Wrapper direction="vertical">
|
||||||
<Title>Export Panel</Title>
|
<Title>Export Panel</Title>
|
||||||
@ -81,14 +90,15 @@ function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
|
|||||||
<DashboardSelect
|
<DashboardSelect
|
||||||
placeholder="Select Dashboard"
|
placeholder="Select Dashboard"
|
||||||
options={options}
|
options={options}
|
||||||
loading={isLoading || createDashboardLoading}
|
loading={isDashboardLoading}
|
||||||
disabled={isLoading || createDashboardLoading}
|
disabled={isDashboardLoading}
|
||||||
value={selectedDashboardId}
|
value={selectedDashboardId}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
disabled={isLoading || !options?.length || !selectedDashboardId}
|
loading={isLoading}
|
||||||
|
disabled={isDisabled}
|
||||||
onClick={handleExportClick}
|
onClick={handleExportClick}
|
||||||
>
|
>
|
||||||
Export
|
Export
|
||||||
|
@ -1,24 +1,44 @@
|
|||||||
import { Button, Dropdown, MenuProps, Modal } from 'antd';
|
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 { useCallback, useMemo, useState } from 'react';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
import { MENU_KEY, MENU_LABEL } from './config';
|
import { MENU_KEY, MENU_LABEL } from './config';
|
||||||
import ExportPanelContainer from './ExportPanel';
|
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 [isExport, setIsExport] = useState<boolean>(false);
|
||||||
|
|
||||||
const onModalToggle = useCallback((value: boolean) => {
|
const onModalToggle = useCallback((value: boolean) => {
|
||||||
setIsExport(value);
|
setIsExport(value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const onCreateAlertsHandler = useCallback(() => {
|
||||||
|
history.push(
|
||||||
|
`${ROUTES.ALERTS_NEW}?${COMPOSITE_QUERY}=${encodeURIComponent(
|
||||||
|
JSON.stringify(query),
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
const onMenuClickHandler: MenuProps['onClick'] = useCallback(
|
const onMenuClickHandler: MenuProps['onClick'] = useCallback(
|
||||||
(e: OnClickProps) => {
|
(e: OnClickProps) => {
|
||||||
if (e.key === MENU_KEY.EXPORT) {
|
if (e.key === MENU_KEY.EXPORT) {
|
||||||
onModalToggle(true);
|
onModalToggle(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (e.key === MENU_KEY.CREATE_ALERTS) {
|
||||||
|
onCreateAlertsHandler();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[onModalToggle],
|
[onModalToggle, onCreateAlertsHandler],
|
||||||
);
|
);
|
||||||
|
|
||||||
const menu: MenuProps = useMemo(
|
const menu: MenuProps = useMemo(
|
||||||
@ -48,23 +68,34 @@ function ExportPanel({ onExport }: ExportPanelProps): JSX.Element {
|
|||||||
<Button>Actions</Button>
|
<Button>Actions</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Modal
|
<Modal
|
||||||
|
footer={null}
|
||||||
onOk={onCancel(false)}
|
onOk={onCancel(false)}
|
||||||
onCancel={onCancel(false)}
|
onCancel={onCancel(false)}
|
||||||
open={isExport}
|
open={isExport}
|
||||||
centered
|
centered
|
||||||
>
|
>
|
||||||
<ExportPanelContainer onExport={onExport} />
|
<ExportPanelContainer
|
||||||
|
query={query}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onExport={onExport}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExportPanel.defaultProps = {
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
|
||||||
interface OnClickProps {
|
interface OnClickProps {
|
||||||
key: string;
|
key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExportPanelProps {
|
export interface ExportPanelProps {
|
||||||
|
isLoading?: boolean;
|
||||||
onExport: (dashboard: Dashboard | null) => void;
|
onExport: (dashboard: Dashboard | null) => void;
|
||||||
|
query: Query | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExportPanel;
|
export default ExportPanel;
|
||||||
|
@ -8,6 +8,7 @@ import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
|
|||||||
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
|
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||||
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
|
import { MESSAGE, useIsFeatureDisabled } from 'hooks/useFeatureFlag';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
@ -16,6 +17,8 @@ import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQu
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQueryClient } from 'react-query';
|
import { useQueryClient } from 'react-query';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||||
import {
|
import {
|
||||||
AlertDef,
|
AlertDef,
|
||||||
@ -24,6 +27,7 @@ import {
|
|||||||
} from 'types/api/alerts/def';
|
} from 'types/api/alerts/def';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import BasicInfo from './BasicInfo';
|
import BasicInfo from './BasicInfo';
|
||||||
import ChartPreview from './ChartPreview';
|
import ChartPreview from './ChartPreview';
|
||||||
@ -48,6 +52,10 @@ function FormAlertRules({
|
|||||||
// init namespace for translations
|
// init namespace for translations
|
||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
|
|
||||||
|
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentQuery,
|
currentQuery,
|
||||||
stagedQuery,
|
stagedQuery,
|
||||||
@ -70,16 +78,12 @@ function FormAlertRules({
|
|||||||
|
|
||||||
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
|
const sq = useMemo(() => mapQueryDataFromApi(initQuery), [initQuery]);
|
||||||
|
|
||||||
useShareBuilderUrl({ defaultValue: sq });
|
useShareBuilderUrl(sq);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAlertDef(initialValue);
|
setAlertDef(initialValue);
|
||||||
}, [initialValue]);
|
}, [initialValue]);
|
||||||
|
|
||||||
const onRunQuery = (): void => {
|
|
||||||
handleRunQuery();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCancelHandler = useCallback(() => {
|
const onCancelHandler = useCallback(() => {
|
||||||
history.replace(ROUTES.LIST_ALL_ALERT);
|
history.replace(ROUTES.LIST_ALL_ALERT);
|
||||||
}, []);
|
}, []);
|
||||||
@ -99,7 +103,7 @@ function FormAlertRules({
|
|||||||
}
|
}
|
||||||
const query: Query = { ...currentQuery, queryType: val };
|
const query: Query = { ...currentQuery, queryType: val };
|
||||||
|
|
||||||
redirectWithQueryBuilderData(query);
|
redirectWithQueryBuilderData(updateStepInterval(query, maxTime, minTime));
|
||||||
};
|
};
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
@ -402,7 +406,7 @@ function FormAlertRules({
|
|||||||
queryCategory={currentQuery.queryType}
|
queryCategory={currentQuery.queryType}
|
||||||
setQueryCategory={onQueryCategoryChange}
|
setQueryCategory={onQueryCategoryChange}
|
||||||
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
|
alertType={alertType || AlertTypes.METRICS_BASED_ALERT}
|
||||||
runQuery={onRunQuery}
|
runQuery={handleRunQuery}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<RuleOptions
|
<RuleOptions
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
timePreferance,
|
timePreferance,
|
||||||
} from 'container/NewWidget/RightContainer/timeItems';
|
} from 'container/NewWidget/RightContainer/timeItems';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
|
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import getChartData from 'lib/getChartData';
|
import getChartData from 'lib/getChartData';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
@ -48,11 +49,13 @@ function FullView({
|
|||||||
[selectedTime, globalSelectedTime, widget],
|
[selectedTime, globalSelectedTime, widget],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updatedQuery = useStepInterval(widget?.query);
|
||||||
|
|
||||||
const response = useGetQueryRange(
|
const response = useGetQueryRange(
|
||||||
{
|
{
|
||||||
selectedTime: selectedTime.enum,
|
selectedTime: selectedTime.enum,
|
||||||
graphType: widget.panelTypes,
|
graphType: widget.panelTypes,
|
||||||
query: widget.query,
|
query: updatedQuery,
|
||||||
globalSelectedInterval: globalSelectedTime,
|
globalSelectedInterval: globalSelectedTime,
|
||||||
variables: getDashboardVariables(),
|
variables: getDashboardVariables(),
|
||||||
},
|
},
|
||||||
@ -84,10 +87,8 @@ function FullView({
|
|||||||
{fullViewOptions && (
|
{fullViewOptions && (
|
||||||
<TimeContainer>
|
<TimeContainer>
|
||||||
<TimePreference
|
<TimePreference
|
||||||
{...{
|
selectedTime={selectedTime}
|
||||||
selectedTime,
|
setSelectedTime={setSelectedTime}
|
||||||
setSelectedTime,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
|
@ -4,6 +4,7 @@ import Spinner from 'components/Spinner';
|
|||||||
import GridGraphComponent from 'container/GridGraphComponent';
|
import GridGraphComponent from 'container/GridGraphComponent';
|
||||||
import { UpdateDashboard } from 'container/GridGraphLayout/utils';
|
import { UpdateDashboard } from 'container/GridGraphLayout/utils';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
|
import { useStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import usePreviousValue from 'hooks/usePreviousValue';
|
import usePreviousValue from 'hooks/usePreviousValue';
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
@ -80,11 +81,13 @@ function GridCardGraph({
|
|||||||
const selectedData = selectedDashboard?.data;
|
const selectedData = selectedDashboard?.data;
|
||||||
const { variables } = selectedData;
|
const { variables } = selectedData;
|
||||||
|
|
||||||
|
const updatedQuery = useStepInterval(widget?.query);
|
||||||
|
|
||||||
const queryResponse = useGetQueryRange(
|
const queryResponse = useGetQueryRange(
|
||||||
{
|
{
|
||||||
selectedTime: widget?.timePreferance,
|
selectedTime: widget?.timePreferance,
|
||||||
graphType: widget?.panelTypes,
|
graphType: widget?.panelTypes,
|
||||||
query: widget?.query,
|
query: updatedQuery,
|
||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
variables: getDashboardVariables(),
|
variables: getDashboardVariables(),
|
||||||
},
|
},
|
||||||
|
@ -64,7 +64,7 @@ function WidgetHeader({
|
|||||||
history.push(
|
history.push(
|
||||||
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${
|
`${window.location.pathname}/new?widgetId=${widgetId}&graphType=${
|
||||||
widget.panelTypes
|
widget.panelTypes
|
||||||
}&${COMPOSITE_QUERY}=${JSON.stringify(widget.query)}`,
|
}&${COMPOSITE_QUERY}=${encodeURIComponent(JSON.stringify(widget.query))}`,
|
||||||
);
|
);
|
||||||
}, [widget.id, widget.panelTypes, widget.query]);
|
}, [widget.id, widget.panelTypes, widget.query]);
|
||||||
|
|
||||||
|
@ -77,8 +77,8 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
history.push(
|
history.push(
|
||||||
`${
|
`${
|
||||||
ROUTES.EDIT_ALERTS
|
ROUTES.EDIT_ALERTS
|
||||||
}?ruleId=${record.id.toString()}&${COMPOSITE_QUERY}=${JSON.stringify(
|
}?ruleId=${record.id.toString()}&${COMPOSITE_QUERY}=${encodeURIComponent(
|
||||||
compositeQuery,
|
JSON.stringify(compositeQuery),
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
@ -75,8 +75,8 @@ function ListOfAllDashboard(): JSX.Element {
|
|||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const columns: TableColumnProps<Data>[] = useMemo(
|
const columns = useMemo(() => {
|
||||||
() => [
|
const tableColumns: TableColumnProps<Data>[] = [
|
||||||
{
|
{
|
||||||
title: 'Name',
|
title: 'Name',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
@ -118,19 +118,19 @@ function ListOfAllDashboard(): JSX.Element {
|
|||||||
},
|
},
|
||||||
render: DateComponent,
|
render: DateComponent,
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (action) {
|
if (action) {
|
||||||
columns.push({
|
tableColumns.push({
|
||||||
title: 'Action',
|
title: 'Action',
|
||||||
dataIndex: '',
|
dataIndex: '',
|
||||||
key: 'x',
|
width: 40,
|
||||||
width: 40,
|
render: DeleteButton,
|
||||||
render: DeleteButton,
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
return tableColumns;
|
||||||
|
}, [action]);
|
||||||
|
|
||||||
const data: Data[] = (filteredDashboards || dashboards).map((e) => ({
|
const data: Data[] = (filteredDashboards || dashboards).map((e) => ({
|
||||||
createdBy: e.created_at,
|
createdBy: e.created_at,
|
||||||
|
@ -5,7 +5,9 @@ import Controls from 'container/Controls';
|
|||||||
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
|
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
|
||||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { Pagination } from 'hooks/queryPagination';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
|
import { OrderPreferenceItems } from 'pages/Logs/config';
|
||||||
import * as Papa from 'papaparse';
|
import * as Papa from 'papaparse';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
@ -30,6 +32,7 @@ function LogControls(): JSX.Element | null {
|
|||||||
isLoading: isLogsLoading,
|
isLoading: isLogsLoading,
|
||||||
isLoadingAggregate,
|
isLoadingAggregate,
|
||||||
logs,
|
logs,
|
||||||
|
order,
|
||||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||||
const globalTime = useSelector<AppState, GlobalReducer>(
|
const globalTime = useSelector<AppState, GlobalReducer>(
|
||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
@ -37,7 +40,7 @@ function LogControls(): JSX.Element | null {
|
|||||||
|
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
const handleLogLinesPerPageChange = (e: number): void => {
|
const handleLogLinesPerPageChange = (e: Pagination['limit']): void => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: SET_LOG_LINES_PER_PAGE,
|
type: SET_LOG_LINES_PER_PAGE,
|
||||||
payload: {
|
payload: {
|
||||||
@ -159,6 +162,7 @@ function LogControls(): JSX.Element | null {
|
|||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
size="small"
|
size="small"
|
||||||
type="link"
|
type="link"
|
||||||
|
disabled={order === OrderPreferenceItems.ASC}
|
||||||
onClick={handleGoToLatest}
|
onClick={handleGoToLatest}
|
||||||
>
|
>
|
||||||
<FastBackwardOutlined /> Go to latest
|
<FastBackwardOutlined /> Go to latest
|
||||||
@ -166,7 +170,7 @@ function LogControls(): JSX.Element | null {
|
|||||||
<Divider type="vertical" />
|
<Divider type="vertical" />
|
||||||
<Controls
|
<Controls
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
count={logs.length}
|
totalCount={logs.length}
|
||||||
countPerPage={logLinesPerPage}
|
countPerPage={logLinesPerPage}
|
||||||
handleNavigatePrevious={handleNavigatePrevious}
|
handleNavigatePrevious={handleNavigatePrevious}
|
||||||
handleNavigateNext={handleNavigateNext}
|
handleNavigateNext={handleNavigateNext}
|
||||||
|
@ -2,6 +2,7 @@ import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
|
|||||||
import { Button, Col, Popover } from 'antd';
|
import { Button, Col, Popover } from 'antd';
|
||||||
import getStep from 'lib/getStep';
|
import getStep from 'lib/getStep';
|
||||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||||
|
import { getIdConditions } from 'pages/Logs/utils';
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
@ -45,6 +46,7 @@ function ActionItem({
|
|||||||
idStart,
|
idStart,
|
||||||
liveTail,
|
liveTail,
|
||||||
idEnd,
|
idEnd,
|
||||||
|
order,
|
||||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
|
|
||||||
@ -72,11 +74,10 @@ function ActionItem({
|
|||||||
q: updatedQueryString,
|
q: updatedQueryString,
|
||||||
limit: logLinesPerPage,
|
limit: logLinesPerPage,
|
||||||
orderBy: 'timestamp',
|
orderBy: 'timestamp',
|
||||||
order: 'desc',
|
order,
|
||||||
timestampStart: minTime,
|
timestampStart: minTime,
|
||||||
timestampEnd: maxTime,
|
timestampEnd: maxTime,
|
||||||
...(idStart ? { idGt: idStart } : {}),
|
...getIdConditions(idStart, idEnd, order),
|
||||||
...(idEnd ? { idLt: idEnd } : {}),
|
|
||||||
});
|
});
|
||||||
getLogsAggregate({
|
getLogsAggregate({
|
||||||
timestampStart: minTime,
|
timestampStart: minTime,
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export { LogsExplorerChart } from './LogsExplorerChart';
|
|
@ -2,41 +2,33 @@ import Graph from 'components/Graph';
|
|||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { getExplorerChartData } from 'lib/explorer/getExplorerChartData';
|
import { getExplorerChartData } from 'lib/explorer/getExplorerChartData';
|
||||||
import { useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import { CardStyled } from './LogsExplorerChart.styled';
|
import { CardStyled } from './LogsExplorerChart.styled';
|
||||||
|
|
||||||
export function LogsExplorerChart(): JSX.Element {
|
function LogsExplorerChart(): JSX.Element {
|
||||||
const { stagedQuery } = useQueryBuilder();
|
const { stagedQuery, panelType, isEnabledQuery } = useQueryBuilder();
|
||||||
|
|
||||||
const { selectedTime } = useSelector<AppState, GlobalReducer>(
|
const { selectedTime } = useSelector<AppState, GlobalReducer>(
|
||||||
(state) => state.globalTime,
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
const panelTypeParam = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
|
||||||
|
|
||||||
const { data, isFetching } = useGetQueryRange(
|
const { data, isFetching } = useGetQueryRange(
|
||||||
{
|
{
|
||||||
query: stagedQuery || initialQueriesMap.metrics,
|
query: stagedQuery || initialQueriesMap.metrics,
|
||||||
graphType: panelTypeParam,
|
graphType: panelType || PANEL_TYPES.LIST,
|
||||||
globalSelectedInterval: selectedTime,
|
globalSelectedInterval: selectedTime,
|
||||||
selectedTime: 'GLOBAL_TIME',
|
selectedTime: 'GLOBAL_TIME',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
queryKey: [
|
queryKey: [REACT_QUERY_KEY.GET_QUERY_RANGE, selectedTime, stagedQuery],
|
||||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
enabled: isEnabledQuery,
|
||||||
selectedTime,
|
|
||||||
stagedQuery,
|
|
||||||
panelTypeParam,
|
|
||||||
],
|
|
||||||
enabled: !!stagedQuery,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -64,3 +56,5 @@ export function LogsExplorerChart(): JSX.Element {
|
|||||||
</CardStyled>
|
</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 useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import getStep from 'lib/getStep';
|
import getStep from 'lib/getStep';
|
||||||
import debounce from 'lodash-es/debounce';
|
import debounce from 'lodash-es/debounce';
|
||||||
|
import { getIdConditions } from 'pages/Logs/utils';
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
@ -33,7 +34,7 @@ function SearchFilter({
|
|||||||
const [searchText, setSearchText] = useState(queryString);
|
const [searchText, setSearchText] = useState(queryString);
|
||||||
const [showDropDown, setShowDropDown] = useState(false);
|
const [showDropDown, setShowDropDown] = useState(false);
|
||||||
const searchRef = useRef<InputRef>(null);
|
const searchRef = useRef<InputRef>(null);
|
||||||
const { logLinesPerPage, idEnd, idStart, liveTail } = useSelector<
|
const { logLinesPerPage, idEnd, idStart, liveTail, order } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
ILogsReducer
|
ILogsReducer
|
||||||
>((state) => state.logs);
|
>((state) => state.logs);
|
||||||
@ -99,11 +100,10 @@ function SearchFilter({
|
|||||||
q: customQuery,
|
q: customQuery,
|
||||||
limit: logLinesPerPage,
|
limit: logLinesPerPage,
|
||||||
orderBy: 'timestamp',
|
orderBy: 'timestamp',
|
||||||
order: 'desc',
|
order,
|
||||||
timestampStart: minTime,
|
timestampStart: minTime,
|
||||||
timestampEnd: maxTime,
|
timestampEnd: maxTime,
|
||||||
...(idStart ? { idGt: idStart } : {}),
|
...getIdConditions(idStart, idEnd, order),
|
||||||
...(idEnd ? { idLt: idEnd } : {}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
getLogsAggregate({
|
getLogsAggregate({
|
||||||
@ -128,6 +128,7 @@ function SearchFilter({
|
|||||||
logLinesPerPage,
|
logLinesPerPage,
|
||||||
globalTime,
|
globalTime,
|
||||||
getLogsFields,
|
getLogsFields,
|
||||||
|
order,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -160,6 +161,7 @@ function SearchFilter({
|
|||||||
dispatch,
|
dispatch,
|
||||||
globalTime.maxTime,
|
globalTime.maxTime,
|
||||||
globalTime.minTime,
|
globalTime.minTime,
|
||||||
|
order,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const onPopOverChange = useCallback(
|
const onPopOverChange = useCallback(
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { QueryParams } from 'constants/query';
|
||||||
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
import { getMinMax } from 'container/TopNav/AutoRefresh/config';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
@ -25,6 +26,7 @@ export function useSearchParser(): {
|
|||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
const {
|
const {
|
||||||
searchFilter: { parsedQuery, queryString },
|
searchFilter: { parsedQuery, queryString },
|
||||||
|
order,
|
||||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||||
|
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
@ -39,7 +41,7 @@ export function useSearchParser(): {
|
|||||||
(updatedQueryString: string) => {
|
(updatedQueryString: string) => {
|
||||||
history.replace({
|
history.replace({
|
||||||
pathname: history.location.pathname,
|
pathname: history.location.pathname,
|
||||||
search: `?q=${updatedQueryString}`,
|
search: `?${QueryParams.q}=${updatedQueryString}&${QueryParams.order}=${order}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const globalTime = getMinMax(selectedTime, minTime, maxTime);
|
const globalTime = getMinMax(selectedTime, minTime, maxTime);
|
||||||
|
@ -2,6 +2,8 @@ import {
|
|||||||
initialFormulaBuilderFormValues,
|
initialFormulaBuilderFormValues,
|
||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
|
import getStep from 'lib/getStep';
|
||||||
|
import store from 'store';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import {
|
import {
|
||||||
@ -24,6 +26,11 @@ export const getQueryBuilderQueries = ({
|
|||||||
groupBy,
|
groupBy,
|
||||||
aggregateAttribute: metricName,
|
aggregateAttribute: metricName,
|
||||||
legend,
|
legend,
|
||||||
|
stepInterval: getStep({
|
||||||
|
end: store.getState().globalTime.maxTime,
|
||||||
|
inputFormat: 'ns',
|
||||||
|
start: store.getState().globalTime.minTime,
|
||||||
|
}),
|
||||||
reduceTo: 'sum',
|
reduceTo: 'sum',
|
||||||
filters: {
|
filters: {
|
||||||
items: itemsA,
|
items: itemsA,
|
||||||
@ -64,6 +71,11 @@ export const getQueryBuilderQuerieswithFormula = ({
|
|||||||
items: additionalItemsA,
|
items: additionalItemsA,
|
||||||
op: 'AND',
|
op: 'AND',
|
||||||
},
|
},
|
||||||
|
stepInterval: getStep({
|
||||||
|
end: store.getState().globalTime.maxTime,
|
||||||
|
inputFormat: 'ns',
|
||||||
|
start: store.getState().globalTime.minTime,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...initialQueryBuilderFormValuesMap.metrics,
|
...initialQueryBuilderFormValuesMap.metrics,
|
||||||
@ -79,6 +91,11 @@ export const getQueryBuilderQuerieswithFormula = ({
|
|||||||
items: additionalItemsB,
|
items: additionalItemsB,
|
||||||
op: 'AND',
|
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';
|
} from 'hooks/useResourceAttribute/utils';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
|
||||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||||
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
||||||
import { Button } from './styles';
|
import { Button } from './styles';
|
||||||
import {
|
import {
|
||||||
@ -25,7 +25,7 @@ import {
|
|||||||
onViewTracePopupClick,
|
onViewTracePopupClick,
|
||||||
} from './util';
|
} from './util';
|
||||||
|
|
||||||
function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
function DBCall(): JSX.Element {
|
||||||
const { servicename } = useParams<{ servicename?: string }>();
|
const { servicename } = useParams<{ servicename?: string }>();
|
||||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||||
const { queries } = useResourceAttribute();
|
const { queries } = useResourceAttribute();
|
||||||
@ -59,7 +59,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
|||||||
clickhouse_sql: [],
|
clickhouse_sql: [],
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
}),
|
}),
|
||||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
const databaseCallsAverageDurationWidget = useMemo(
|
const databaseCallsAverageDurationWidget = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -73,7 +73,7 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
|||||||
clickhouse_sql: [],
|
clickhouse_sql: [],
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
}),
|
}),
|
||||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -151,8 +151,4 @@ function DBCall({ getWidgetQueryBuilder }: DBCallProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DBCallProps {
|
|
||||||
getWidgetQueryBuilder: (query: Widgets['query']) => Widgets;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DBCall;
|
export default DBCall;
|
||||||
|
@ -13,10 +13,10 @@ import {
|
|||||||
} from 'hooks/useResourceAttribute/utils';
|
} from 'hooks/useResourceAttribute/utils';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||||
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
import { Card, GraphContainer, GraphTitle, Row } from '../styles';
|
||||||
import { legend } from './constant';
|
import { legend } from './constant';
|
||||||
import { Button } from './styles';
|
import { Button } from './styles';
|
||||||
@ -26,7 +26,7 @@ import {
|
|||||||
onViewTracePopupClick,
|
onViewTracePopupClick,
|
||||||
} from './util';
|
} from './util';
|
||||||
|
|
||||||
function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
function External(): JSX.Element {
|
||||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||||
|
|
||||||
const { servicename } = useParams<{ servicename?: string }>();
|
const { servicename } = useParams<{ servicename?: string }>();
|
||||||
@ -51,7 +51,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
|||||||
clickhouse_sql: [],
|
clickhouse_sql: [],
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
}),
|
}),
|
||||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedTraceTags = useMemo(
|
const selectedTraceTags = useMemo(
|
||||||
@ -71,7 +71,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
|||||||
clickhouse_sql: [],
|
clickhouse_sql: [],
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
}),
|
}),
|
||||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
|
|
||||||
const externalCallRPSWidget = useMemo(
|
const externalCallRPSWidget = useMemo(
|
||||||
@ -87,7 +87,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
|||||||
clickhouse_sql: [],
|
clickhouse_sql: [],
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
}),
|
}),
|
||||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
|
|
||||||
const externalCallDurationAddressWidget = useMemo(
|
const externalCallDurationAddressWidget = useMemo(
|
||||||
@ -103,7 +103,7 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
|||||||
clickhouse_sql: [],
|
clickhouse_sql: [],
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
}),
|
}),
|
||||||
[getWidgetQueryBuilder, servicename, tagFilterItems],
|
[servicename, tagFilterItems],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -261,8 +261,4 @@ function External({ getWidgetQueryBuilder }: ExternalProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ExternalProps {
|
|
||||||
getWidgetQueryBuilder: (query: Widgets['query']) => Widgets;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default External;
|
export default External;
|
||||||
|
@ -18,11 +18,11 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
import { UpdateTimeInterval } from 'store/actions';
|
import { UpdateTimeInterval } from 'store/actions';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { Widgets } from 'types/api/dashboard/getAll';
|
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import MetricReducer from 'types/reducer/metrics';
|
import MetricReducer from 'types/reducer/metrics';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||||
import {
|
import {
|
||||||
errorPercentage,
|
errorPercentage,
|
||||||
operationPerSec,
|
operationPerSec,
|
||||||
@ -36,7 +36,7 @@ import {
|
|||||||
onViewTracePopupClick,
|
onViewTracePopupClick,
|
||||||
} from './util';
|
} from './util';
|
||||||
|
|
||||||
function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
function Application(): JSX.Element {
|
||||||
const { servicename } = useParams<{ servicename?: string }>();
|
const { servicename } = useParams<{ servicename?: string }>();
|
||||||
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
const [selectedTimeStamp, setSelectedTimeStamp] = useState<number>(0);
|
||||||
const { search } = useLocation();
|
const { search } = useLocation();
|
||||||
@ -94,7 +94,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
|||||||
clickhouse_sql: [],
|
clickhouse_sql: [],
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
}),
|
}),
|
||||||
[getWidgetQueryBuilder, servicename, topLevelOperations, tagFilterItems],
|
[servicename, topLevelOperations, tagFilterItems],
|
||||||
);
|
);
|
||||||
|
|
||||||
const errorPercentageWidget = useMemo(
|
const errorPercentageWidget = useMemo(
|
||||||
@ -110,7 +110,7 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
|||||||
clickhouse_sql: [],
|
clickhouse_sql: [],
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
}),
|
}),
|
||||||
[servicename, topLevelOperations, tagFilterItems, getWidgetQueryBuilder],
|
[servicename, topLevelOperations, tagFilterItems],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onDragSelect = useCallback(
|
const onDragSelect = useCallback(
|
||||||
@ -289,10 +289,6 @@ function Application({ getWidgetQueryBuilder }: DashboardProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DashboardProps {
|
|
||||||
getWidgetQueryBuilder: (query: Widgets['query']) => Widgets;
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClickHandlerType = (
|
type ClickHandlerType = (
|
||||||
ChartEvent: ChartEvent,
|
ChartEvent: ChartEvent,
|
||||||
activeElements: ActiveElement[],
|
activeElements: ActiveElement[],
|
||||||
|
@ -6,21 +6,20 @@ import { memo, useMemo } from 'react';
|
|||||||
import { generatePath, useParams } from 'react-router-dom';
|
import { generatePath, useParams } from 'react-router-dom';
|
||||||
import { useLocation } from 'react-use';
|
import { useLocation } from 'react-use';
|
||||||
|
|
||||||
import { getWidgetQueryBuilder } from './MetricsApplication.factory';
|
|
||||||
import DBCall from './Tabs/DBCall';
|
import DBCall from './Tabs/DBCall';
|
||||||
import External from './Tabs/External';
|
import External from './Tabs/External';
|
||||||
import Overview from './Tabs/Overview';
|
import Overview from './Tabs/Overview';
|
||||||
|
|
||||||
function OverViewTab(): JSX.Element {
|
function OverViewTab(): JSX.Element {
|
||||||
return <Overview getWidgetQueryBuilder={getWidgetQueryBuilder} />;
|
return <Overview />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DbCallTab(): JSX.Element {
|
function DbCallTab(): JSX.Element {
|
||||||
return <DBCall getWidgetQueryBuilder={getWidgetQueryBuilder} />;
|
return <DBCall />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExternalTab(): JSX.Element {
|
function ExternalTab(): JSX.Element {
|
||||||
return <External getWidgetQueryBuilder={getWidgetQueryBuilder} />;
|
return <External />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ServiceMetrics(): JSX.Element {
|
function ServiceMetrics(): JSX.Element {
|
||||||
|
@ -47,7 +47,9 @@ function DashboardGraphSlider({ toggleAddWidget }: Props): JSX.Element {
|
|||||||
history.push(
|
history.push(
|
||||||
`${history.location.pathname}/new?graphType=${name}&widgetId=${
|
`${history.location.pathname}/new?graphType=${name}&widgetId=${
|
||||||
emptyLayout.i
|
emptyLayout.i
|
||||||
}&${COMPOSITE_QUERY}=${JSON.stringify(initialQueriesMap.metrics)}`,
|
}&${COMPOSITE_QUERY}=${encodeURIComponent(
|
||||||
|
JSON.stringify(initialQueriesMap.metrics),
|
||||||
|
)}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.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 {
|
interface ItemsProps {
|
||||||
name: ITEMS;
|
name: ITEMS;
|
||||||
|
@ -6,6 +6,7 @@ import { QueryBuilder } from 'container/QueryBuilder';
|
|||||||
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
|
import { useGetWidgetQueryRange } from 'hooks/queryBuilder/useGetWidgetQueryRange';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
|
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { connect, useSelector } from 'react-redux';
|
||||||
@ -22,6 +23,7 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
|||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
import DashboardReducer from 'types/reducer/dashboards';
|
import DashboardReducer from 'types/reducer/dashboards';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
|
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
|
||||||
import PromQLQueryContainer from './QueryBuilder/promQL';
|
import PromQLQueryContainer from './QueryBuilder/promQL';
|
||||||
@ -33,6 +35,11 @@ function QuerySection({
|
|||||||
}: QueryProps): JSX.Element {
|
}: QueryProps): JSX.Element {
|
||||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
|
|
||||||
|
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
);
|
);
|
||||||
@ -58,7 +65,7 @@ function QuerySection({
|
|||||||
|
|
||||||
const { query } = selectedWidget;
|
const { query } = selectedWidget;
|
||||||
|
|
||||||
useShareBuilderUrl({ defaultValue: query });
|
useShareBuilderUrl(query);
|
||||||
|
|
||||||
const handleStageQuery = useCallback(
|
const handleStageQuery = useCallback(
|
||||||
(updatedQuery: Query): void => {
|
(updatedQuery: Query): void => {
|
||||||
@ -67,10 +74,19 @@ function QuerySection({
|
|||||||
yAxisUnit: selectedWidget.yAxisUnit,
|
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 => {
|
const handleQueryCategoryChange = (qCategory: string): void => {
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import { Input } from 'antd';
|
import { Input } from 'antd';
|
||||||
|
import Typography from 'antd/es/typography/Typography';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { OptionsMenuConfig } from '..';
|
|
||||||
import { FieldTitle } from '../styles';
|
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 {
|
function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
|
||||||
const { t } = useTranslation(['trace']);
|
const { t } = useTranslation(['trace']);
|
||||||
@ -19,19 +26,24 @@ function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
|
|||||||
|
|
||||||
<Input.Group compact>
|
<Input.Group compact>
|
||||||
<AddColumnSelect
|
<AddColumnSelect
|
||||||
allowClear
|
|
||||||
maxTagCount={0}
|
|
||||||
size="small"
|
size="small"
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
options={config.options}
|
options={config.options}
|
||||||
value={config.value}
|
value={[]}
|
||||||
onChange={config.onChange}
|
onChange={config.onChange}
|
||||||
/>
|
/>
|
||||||
<SearchIconWrapper $isDarkMode={isDarkMode}>
|
<SearchIconWrapper $isDarkMode={isDarkMode}>
|
||||||
<SearchOutlined />
|
<SearchOutlined />
|
||||||
</SearchIconWrapper>
|
</SearchIconWrapper>
|
||||||
</Input.Group>
|
</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>
|
</AddColumnWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
import { Card, Select, SelectProps, Space } from 'antd';
|
import { Card, Select, SelectProps, Space } from 'antd';
|
||||||
import { themeColors } from 'constants/theme';
|
import { themeColors } from 'constants/theme';
|
||||||
import { FunctionComponent } from 'react';
|
import { FunctionComponent } from 'react';
|
||||||
@ -26,3 +27,13 @@ export const AddColumnSelect: FunctionComponent<SelectProps> = styled(
|
|||||||
export const AddColumnWrapper = styled(Space)`
|
export const AddColumnWrapper = styled(Space)`
|
||||||
width: 100%;
|
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 { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { OptionsMenuConfig } from '..';
|
|
||||||
import { FieldTitle } from '../styles';
|
import { FieldTitle } from '../styles';
|
||||||
|
import { OptionsMenuConfig } from '../types';
|
||||||
import { FormatFieldWrapper, RadioButton, RadioGroup } from './styles';
|
import { FormatFieldWrapper, RadioButton, RadioGroup } from './styles';
|
||||||
|
|
||||||
function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { OptionsMenuConfig } from '..';
|
|
||||||
import { FieldTitle } from '../styles';
|
import { FieldTitle } from '../styles';
|
||||||
|
import { OptionsMenuConfig } from '../types';
|
||||||
import { MaxLinesFieldWrapper, MaxLinesInput } from './styles';
|
import { MaxLinesFieldWrapper, MaxLinesInput } from './styles';
|
||||||
|
|
||||||
function MaxLinesField({ config }: MaxLinesFieldProps): JSX.Element | null {
|
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 { SettingFilled, SettingOutlined } from '@ant-design/icons';
|
||||||
import {
|
import { Popover, Space } from 'antd';
|
||||||
InputNumberProps,
|
|
||||||
Popover,
|
|
||||||
RadioProps,
|
|
||||||
SelectProps,
|
|
||||||
Space,
|
|
||||||
} from 'antd';
|
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -14,6 +8,12 @@ import AddColumnField from './AddColumnField';
|
|||||||
import FormatField from './FormatField';
|
import FormatField from './FormatField';
|
||||||
import MaxLinesField from './MaxLinesField';
|
import MaxLinesField from './MaxLinesField';
|
||||||
import { OptionsContainer, OptionsContentWrapper } from './styles';
|
import { OptionsContainer, OptionsContentWrapper } from './styles';
|
||||||
|
import { OptionsMenuConfig } from './types';
|
||||||
|
import useOptionsMenu from './useOptionsMenu';
|
||||||
|
|
||||||
|
interface OptionsMenuProps {
|
||||||
|
config: OptionsMenuConfig;
|
||||||
|
}
|
||||||
|
|
||||||
function OptionsMenu({ config }: OptionsMenuProps): JSX.Element {
|
function OptionsMenu({ config }: OptionsMenuProps): JSX.Element {
|
||||||
const { t } = useTranslation(['trace']);
|
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 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({
|
export const QueryBuilder = memo(function QueryBuilder({
|
||||||
config,
|
config,
|
||||||
panelType,
|
panelType: newPanelType,
|
||||||
actions,
|
actions,
|
||||||
}: QueryBuilderProps): JSX.Element {
|
}: QueryBuilderProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
currentQuery,
|
currentQuery,
|
||||||
setupInitialDataSource,
|
|
||||||
addNewBuilderQuery,
|
addNewBuilderQuery,
|
||||||
addNewFormula,
|
addNewFormula,
|
||||||
handleSetPanelType,
|
handleSetConfig,
|
||||||
|
panelType,
|
||||||
|
initialDataSource,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
useEffect(() => {
|
const currentDataSource = useMemo(
|
||||||
if (config && config.queryVariant === 'static') {
|
() =>
|
||||||
setupInitialDataSource(config.initialDataSource);
|
(config && config.queryVariant === 'static' && config.initialDataSource) ||
|
||||||
}
|
null,
|
||||||
}, [config, setupInitialDataSource]);
|
[config],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleSetPanelType(panelType);
|
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
|
||||||
}, [handleSetPanelType, panelType]);
|
handleSetConfig(newPanelType, currentDataSource);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
handleSetConfig,
|
||||||
|
panelType,
|
||||||
|
initialDataSource,
|
||||||
|
currentDataSource,
|
||||||
|
newPanelType,
|
||||||
|
]);
|
||||||
|
|
||||||
const isDisabledQueryButton = useMemo(
|
const isDisabledQueryButton = useMemo(
|
||||||
() => currentQuery.builder.queryData.length >= MAX_QUERIES,
|
() => currentQuery.builder.queryData.length >= MAX_QUERIES,
|
||||||
@ -48,7 +58,7 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
|
|
||||||
const isAvailableToDisableQuery = useMemo(
|
const isAvailableToDisableQuery = useMemo(
|
||||||
() =>
|
() =>
|
||||||
currentQuery.builder.queryData.length > 1 ||
|
currentQuery.builder.queryData.length > 0 ||
|
||||||
currentQuery.builder.queryFormulas.length > 0,
|
currentQuery.builder.queryFormulas.length > 0,
|
||||||
[currentQuery],
|
[currentQuery],
|
||||||
);
|
);
|
||||||
|
@ -40,6 +40,7 @@ export const Query = memo(function Query({
|
|||||||
const {
|
const {
|
||||||
operators,
|
operators,
|
||||||
isMetricsDataSource,
|
isMetricsDataSource,
|
||||||
|
isTracePanelType,
|
||||||
listOfAdditionalFilters,
|
listOfAdditionalFilters,
|
||||||
handleChangeAggregatorAttribute,
|
handleChangeAggregatorAttribute,
|
||||||
handleChangeDataSource,
|
handleChangeDataSource,
|
||||||
@ -196,7 +197,56 @@ export const Query = memo(function Query({
|
|||||||
}
|
}
|
||||||
|
|
||||||
default: {
|
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>
|
||||||
<Col flex="1 1 12.5rem">
|
<Col flex="1 1 12.5rem">
|
||||||
<AggregatorFilter
|
<AggregatorFilter
|
||||||
onChange={handleChangeAggregatorAttribute}
|
|
||||||
query={query}
|
query={query}
|
||||||
|
onChange={handleChangeAggregatorAttribute}
|
||||||
|
disabled={
|
||||||
|
panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@ -305,21 +358,25 @@ export const Query = memo(function Query({
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={24}>
|
{!isTracePanelType && (
|
||||||
<AdditionalFiltersToggler listOfAdditionalFilter={listOfAdditionalFilters}>
|
<Col span={24}>
|
||||||
<Row gutter={[0, 11]} justify="space-between">
|
<AdditionalFiltersToggler listOfAdditionalFilter={listOfAdditionalFilters}>
|
||||||
{renderAdditionalFilters()}
|
<Row gutter={[0, 11]} justify="space-between">
|
||||||
</Row>
|
{renderAdditionalFilters()}
|
||||||
</AdditionalFiltersToggler>
|
</Row>
|
||||||
</Col>
|
</AdditionalFiltersToggler>
|
||||||
<Row style={{ width: '100%' }}>
|
</Col>
|
||||||
<Input
|
)}
|
||||||
onChange={handleChangeQueryLegend}
|
{panelType !== PANEL_TYPES.LIST && panelType !== PANEL_TYPES.TRACE && (
|
||||||
size="middle"
|
<Row style={{ width: '100%' }}>
|
||||||
value={query.legend}
|
<Input
|
||||||
addonBefore="Legend Format"
|
onChange={handleChangeQueryLegend}
|
||||||
/>
|
size="middle"
|
||||||
</Row>
|
value={query.legend}
|
||||||
|
addonBefore="Legend Format"
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
</ListItemWrapper>
|
</ListItemWrapper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import { Input } from 'antd';
|
import { InputNumber, InputNumberProps } from 'antd';
|
||||||
import getStep from 'lib/getStep';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
|
||||||
|
|
||||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
|
|
||||||
@ -13,49 +9,27 @@ function AggregateEveryFilter({
|
|||||||
onChange,
|
onChange,
|
||||||
query,
|
query,
|
||||||
}: AggregateEveryFilterProps): JSX.Element {
|
}: 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(
|
const isMetricsDataSource = useMemo(
|
||||||
() => query.dataSource === DataSource.METRICS,
|
() => query.dataSource === DataSource.METRICS,
|
||||||
[query.dataSource],
|
[query.dataSource],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onChangeHandler: InputNumberProps<number>['onChange'] = (event) => {
|
||||||
|
if (event && event >= 0) {
|
||||||
|
onChange(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDisabled = isMetricsDataSource && !query.aggregateAttribute.key;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input
|
<InputNumber
|
||||||
type="text"
|
|
||||||
placeholder="Enter in seconds"
|
placeholder="Enter in seconds"
|
||||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
disabled={isDisabled}
|
||||||
style={selectStyle}
|
style={selectStyle}
|
||||||
defaultValue={query.stepInterval ?? stepInterval}
|
value={query.stepInterval}
|
||||||
onChange={(event): void => onChange(Number(event.target.value))}
|
onChange={onChangeHandler}
|
||||||
onKeyDown={handleKeyDown}
|
min={0}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
|
import { AutoCompleteProps } from 'antd';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
export type AgregatorFilterProps = {
|
export type AgregatorFilterProps = Pick<AutoCompleteProps, 'disabled'> & {
|
||||||
onChange: (value: BaseAutocompleteData) => void;
|
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
|
onChange: (value: BaseAutocompleteData) => void;
|
||||||
};
|
};
|
||||||
|
@ -28,8 +28,9 @@ import { selectStyle } from '../QueryBuilderSearch/config';
|
|||||||
import { AgregatorFilterProps } from './AggregatorFilter.intefaces';
|
import { AgregatorFilterProps } from './AggregatorFilter.intefaces';
|
||||||
|
|
||||||
export const AggregatorFilter = memo(function AggregatorFilter({
|
export const AggregatorFilter = memo(function AggregatorFilter({
|
||||||
onChange,
|
|
||||||
query,
|
query,
|
||||||
|
disabled,
|
||||||
|
onChange,
|
||||||
}: AgregatorFilterProps): JSX.Element {
|
}: AgregatorFilterProps): JSX.Element {
|
||||||
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
|
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
|
||||||
const debouncedValue = useDebounce(query.aggregateAttribute.key, 300);
|
const debouncedValue = useDebounce(query.aggregateAttribute.key, 300);
|
||||||
@ -119,6 +120,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
options={optionsData}
|
options={optionsData}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChangeAttribute}
|
onChange={handleChangeAttribute}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -102,10 +102,12 @@ export const GroupByFilter = memo(function GroupByFilter({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
key,
|
key: key || currentValue,
|
||||||
dataType: dataType as DataType,
|
dataType: (dataType as DataType) || initialAutocompleteData.dataType,
|
||||||
type: type as AutocompleteType,
|
type: (type as AutocompleteType) || initialAutocompleteData.type,
|
||||||
isColumn: isColumn === 'true',
|
isColumn: isColumn
|
||||||
|
? isColumn === 'true'
|
||||||
|
: initialAutocompleteData.isColumn,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Select, Spin } from 'antd';
|
import { Select, Spin } from 'antd';
|
||||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||||
|
import { IOption } from 'hooks/useResourceAttribute/types';
|
||||||
|
import { uniqWith } from 'lodash-es';
|
||||||
import * as Papa from 'papaparse';
|
import * as Papa from 'papaparse';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery } from 'react-query';
|
||||||
@ -9,12 +11,14 @@ import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
|||||||
|
|
||||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
import { getRemoveOrderFromValue } from '../QueryBuilderSearch/utils';
|
import { getRemoveOrderFromValue } from '../QueryBuilderSearch/utils';
|
||||||
|
import { FILTERS } from './config';
|
||||||
import { OrderByFilterProps } from './OrderByFilter.interfaces';
|
import { OrderByFilterProps } from './OrderByFilter.interfaces';
|
||||||
import {
|
import {
|
||||||
checkIfKeyPresent,
|
checkIfKeyPresent,
|
||||||
getLabelFromValue,
|
getLabelFromValue,
|
||||||
mapLabelValuePairs,
|
mapLabelValuePairs,
|
||||||
orderByValueDelimiter,
|
orderByValueDelimiter,
|
||||||
|
transformToOrderByStringValues,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
export function OrderByFilter({
|
export function OrderByFilter({
|
||||||
@ -22,7 +26,9 @@ export function OrderByFilter({
|
|||||||
onChange,
|
onChange,
|
||||||
}: OrderByFilterProps): JSX.Element {
|
}: OrderByFilterProps): JSX.Element {
|
||||||
const [searchText, setSearchText] = useState<string>('');
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
const [selectedValue, setSelectedValue] = useState<string[]>([]);
|
const [selectedValue, setSelectedValue] = useState<IOption[]>(
|
||||||
|
transformToOrderByStringValues(query.orderBy) || [],
|
||||||
|
);
|
||||||
|
|
||||||
const { data, isFetching } = useQuery(
|
const { data, isFetching } = useQuery(
|
||||||
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText],
|
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText],
|
||||||
@ -55,23 +61,41 @@ export function OrderByFilter({
|
|||||||
.flat()
|
.flat()
|
||||||
.concat([
|
.concat([
|
||||||
{
|
{
|
||||||
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) asc`,
|
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.ASC}`,
|
||||||
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}asc`,
|
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}${FILTERS.ASC}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) desc`,
|
label: `${query.aggregateOperator}(${query.aggregateAttribute.key}) ${FILTERS.DESC}`,
|
||||||
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}desc`,
|
value: `${query.aggregateOperator}(${query.aggregateAttribute.key})${orderByValueDelimiter}${FILTERS.DESC}`,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
[query.aggregateAttribute.key, query.aggregateOperator, query.groupBy],
|
[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 optionsData = useMemo(() => {
|
||||||
const options =
|
const options =
|
||||||
query.aggregateOperator === MetricAggregateOperator.NOOP
|
query.aggregateOperator === MetricAggregateOperator.NOOP
|
||||||
? noAggregationOptions
|
? noAggregationOptions
|
||||||
: aggregationOptions;
|
: aggregationOptions;
|
||||||
return options.filter(
|
|
||||||
|
const resultOption = [...customValue, ...options];
|
||||||
|
|
||||||
|
return resultOption.filter(
|
||||||
(option) =>
|
(option) =>
|
||||||
!getLabelFromValue(selectedValue).includes(
|
!getLabelFromValue(selectedValue).includes(
|
||||||
getRemoveOrderFromValue(option.value),
|
getRemoveOrderFromValue(option.value),
|
||||||
@ -79,30 +103,58 @@ export function OrderByFilter({
|
|||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
aggregationOptions,
|
aggregationOptions,
|
||||||
|
customValue,
|
||||||
noAggregationOptions,
|
noAggregationOptions,
|
||||||
query.aggregateOperator,
|
query.aggregateOperator,
|
||||||
selectedValue,
|
selectedValue,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleChange = (values: string[]): void => {
|
const getUniqValues = useCallback((values: IOption[]): IOption[] => {
|
||||||
setSelectedValue(values);
|
const modifiedValues = values.map((item) => {
|
||||||
const orderByValues: OrderByPayload[] = values.map((item) => {
|
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
|
||||||
const match = Papa.parse(item, { delimiter: '|' });
|
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) {
|
if (match) {
|
||||||
const [columnName, order] = match.data.flat() as string[];
|
const [columnName, order] = match.data.flat() as string[];
|
||||||
return {
|
return {
|
||||||
columnName: checkIfKeyPresent(columnName, query.aggregateAttribute.key)
|
columnName: checkIfKeyPresent(columnName, query.aggregateAttribute.key)
|
||||||
? '#SIGNOZ_VALUE'
|
? '#SIGNOZ_VALUE'
|
||||||
: columnName,
|
: columnName,
|
||||||
order,
|
order: order ?? 'asc',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columnName: item,
|
columnName: item.value,
|
||||||
order: '',
|
order: 'asc',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setSearchText('');
|
||||||
onChange(orderByValues);
|
onChange(orderByValues);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -126,6 +178,8 @@ export function OrderByFilter({
|
|||||||
showSearch
|
showSearch
|
||||||
disabled={isMetricsDataSource && isDisabledSelect}
|
disabled={isMetricsDataSource && isDisabledSelect}
|
||||||
showArrow={false}
|
showArrow={false}
|
||||||
|
value={selectedValue}
|
||||||
|
labelInValue
|
||||||
filterOption={false}
|
filterOption={false}
|
||||||
options={optionsData}
|
options={optionsData}
|
||||||
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
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 { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||||
import * as Papa from 'papaparse';
|
import * as Papa from 'papaparse';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
export const orderByValueDelimiter = '|';
|
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(
|
export function mapLabelValuePairs(
|
||||||
arr: BaseAutocompleteData[],
|
arr: BaseAutocompleteData[],
|
||||||
): Array<IOption>[] {
|
): 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) => {
|
return arr.flat().map((item) => {
|
||||||
const match = Papa.parse(item, { delimiter: orderByValueDelimiter });
|
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
|
||||||
if (match) {
|
if (match) {
|
||||||
const [key] = match.data as string[];
|
const [key] = match.data as string[];
|
||||||
return key[0];
|
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 setLocalStorageKey from 'api/browser/localstorage/set';
|
||||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import dayjs, { Dayjs } from 'dayjs';
|
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 getTimeString from 'lib/getTimeString';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { connect, useSelector } from 'react-redux';
|
||||||
@ -66,6 +69,8 @@ function DateTimeSelection({
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { stagedQuery, initQueryBuilderData } = useQueryBuilder();
|
||||||
|
|
||||||
const { maxTime, minTime, selectedTime } = useSelector<
|
const { maxTime, minTime, selectedTime } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
@ -174,6 +179,14 @@ function DateTimeSelection({
|
|||||||
setRefreshButtonHidden(true);
|
setRefreshButtonHidden(true);
|
||||||
setCustomDTPickerVisible(true);
|
setCustomDTPickerVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!stagedQuery) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { maxTime, minTime } = GetMinMax(value, getTime());
|
||||||
|
|
||||||
|
initQueryBuilderData(updateStepInterval(stagedQuery, maxTime, minTime));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRefreshHandler = (): void => {
|
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 { memo } from 'react';
|
||||||
|
|
||||||
import { Container } from './styles';
|
import { Container } from './styles';
|
||||||
|
|
||||||
function TraceExplorerControls(): JSX.Element | null {
|
function TraceExplorerControls({
|
||||||
const handleCountItemsPerPageChange = (): void => {};
|
isLoading,
|
||||||
const handleNavigatePrevious = (): void => {};
|
totalCount,
|
||||||
const handleNavigateNext = (): void => {};
|
perPageOptions,
|
||||||
|
config,
|
||||||
|
}: TraceExplorerControlsProps): JSX.Element | null {
|
||||||
|
const {
|
||||||
|
pagination,
|
||||||
|
handleCountItemsPerPageChange,
|
||||||
|
handleNavigateNext,
|
||||||
|
handleNavigatePrevious,
|
||||||
|
} = useQueryPagination(totalCount, perPageOptions);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
|
{config && <OptionsMenu config={{ addColumn: config?.addColumn }} />}
|
||||||
|
|
||||||
<Controls
|
<Controls
|
||||||
isLoading={false}
|
isLoading={isLoading}
|
||||||
count={0}
|
totalCount={totalCount}
|
||||||
countPerPage={0}
|
offset={pagination.offset}
|
||||||
handleNavigatePrevious={handleNavigatePrevious}
|
countPerPage={pagination.limit}
|
||||||
handleNavigateNext={handleNavigateNext}
|
perPageOptions={perPageOptions}
|
||||||
handleCountItemsPerPageChange={handleCountItemsPerPageChange}
|
handleCountItemsPerPageChange={handleCountItemsPerPageChange}
|
||||||
|
handleNavigateNext={handleNavigateNext}
|
||||||
|
handleNavigatePrevious={handleNavigatePrevious}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TraceExplorerControls.defaultProps = {
|
||||||
|
config: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
type TraceExplorerControlsProps = Pick<
|
||||||
|
ControlsProps,
|
||||||
|
'isLoading' | 'totalCount' | 'perPageOptions'
|
||||||
|
> & {
|
||||||
|
config?: OptionsMenuConfig | null;
|
||||||
|
};
|
||||||
|
|
||||||
export default memo(TraceExplorerControls);
|
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 { Button } from 'antd';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
|
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { memo } from 'react';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { ButtonWrapper, Container } from './styles';
|
import { ButtonWrapper, Container } from './styles';
|
||||||
@ -9,10 +11,12 @@ import { ButtonWrapper, Container } from './styles';
|
|||||||
function QuerySection(): JSX.Element {
|
function QuerySection(): JSX.Element {
|
||||||
const { handleRunQuery } = useQueryBuilder();
|
const { handleRunQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<QueryBuilder
|
<QueryBuilder
|
||||||
panelType={PANEL_TYPES.TIME_SERIES}
|
panelType={panelTypes}
|
||||||
config={{
|
config={{
|
||||||
queryVariant: 'static',
|
queryVariant: 'static',
|
||||||
initialDataSource: DataSource.TRACES,
|
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(() => {
|
return useMemo(() => {
|
||||||
const compositeQuery = urlQuery.get(COMPOSITE_QUERY);
|
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]);
|
}, [urlQuery]);
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
|
||||||
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables';
|
||||||
import { UseQueryOptions, UseQueryResult } from 'react-query';
|
import { UseQueryOptions, UseQueryResult } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
@ -11,6 +10,7 @@ import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
|||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import { useGetQueryRange } from './useGetQueryRange';
|
import { useGetQueryRange } from './useGetQueryRange';
|
||||||
|
import { useQueryBuilder } from './useQueryBuilder';
|
||||||
|
|
||||||
export const useGetWidgetQueryRange = (
|
export const useGetWidgetQueryRange = (
|
||||||
{
|
{
|
||||||
@ -19,31 +19,29 @@ export const useGetWidgetQueryRange = (
|
|||||||
}: Pick<GetQueryResultsProps, 'graphType' | 'selectedTime'>,
|
}: Pick<GetQueryResultsProps, 'graphType' | 'selectedTime'>,
|
||||||
options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
|
options?: UseQueryOptions<SuccessResponse<MetricRangePayloadProps>, Error>,
|
||||||
): UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error> => {
|
): UseQueryResult<SuccessResponse<MetricRangePayloadProps>, Error> => {
|
||||||
const urlQuery = useUrlQuery();
|
|
||||||
|
|
||||||
const { selectedTime: globalSelectedInterval } = useSelector<
|
const { selectedTime: globalSelectedInterval } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
const compositeQuery = urlQuery.get(COMPOSITE_QUERY);
|
const { stagedQuery } = useQueryBuilder();
|
||||||
|
|
||||||
return useGetQueryRange(
|
return useGetQueryRange(
|
||||||
{
|
{
|
||||||
graphType,
|
graphType,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
query: JSON.parse(compositeQuery || '{}'),
|
query: stagedQuery || initialQueriesMap.metrics,
|
||||||
variables: getDashboardVariables(),
|
variables: getDashboardVariables(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: !!compositeQuery,
|
enabled: !!stagedQuery,
|
||||||
retry: false,
|
retry: false,
|
||||||
queryKey: [
|
queryKey: [
|
||||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
globalSelectedInterval,
|
globalSelectedInterval,
|
||||||
compositeQuery,
|
stagedQuery,
|
||||||
],
|
],
|
||||||
...options,
|
...options,
|
||||||
},
|
},
|
||||||
|
@ -2,6 +2,7 @@ import {
|
|||||||
initialAutocompleteData,
|
initialAutocompleteData,
|
||||||
initialQueryBuilderFormValuesMap,
|
initialQueryBuilderFormValuesMap,
|
||||||
mapOfFilters,
|
mapOfFilters,
|
||||||
|
PANEL_TYPES,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
|
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
|
||||||
@ -56,9 +57,16 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const getNewListOfAdditionalFilters = useCallback(
|
const getNewListOfAdditionalFilters = useCallback(
|
||||||
(dataSource: DataSource): string[] =>
|
(dataSource: DataSource): string[] => {
|
||||||
mapOfFilters[dataSource].map((item) => item.text),
|
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(
|
const handleChangeAggregatorAttribute = useCallback(
|
||||||
@ -78,7 +86,7 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
|
|||||||
(nextSource: DataSource): void => {
|
(nextSource: DataSource): void => {
|
||||||
const newOperators = getOperatorsBySourceAndPanelType({
|
const newOperators = getOperatorsBySourceAndPanelType({
|
||||||
dataSource: nextSource,
|
dataSource: nextSource,
|
||||||
panelType,
|
panelType: panelType || PANEL_TYPES.TIME_SERIES,
|
||||||
});
|
});
|
||||||
|
|
||||||
const entries = Object.entries(
|
const entries = Object.entries(
|
||||||
@ -121,33 +129,22 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
|
|||||||
[query.dataSource],
|
[query.dataSource],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isTracePanelType = useMemo(() => panelType === PANEL_TYPES.TRACE, [
|
||||||
|
panelType,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialDataSource && dataSource !== initialDataSource) return;
|
if (initialDataSource && dataSource !== initialDataSource) return;
|
||||||
|
|
||||||
const initialOperators = getOperatorsBySourceAndPanelType({
|
const initialOperators = getOperatorsBySourceAndPanelType({
|
||||||
dataSource,
|
dataSource,
|
||||||
panelType,
|
panelType: panelType || PANEL_TYPES.TIME_SERIES,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (JSON.stringify(operators) === JSON.stringify(initialOperators)) return;
|
if (JSON.stringify(operators) === JSON.stringify(initialOperators)) return;
|
||||||
|
|
||||||
setOperators(initialOperators);
|
setOperators(initialOperators);
|
||||||
|
}, [dataSource, initialDataSource, panelType, operators]);
|
||||||
const isCurrentOperatorAvailableInList = initialOperators
|
|
||||||
.map((operator) => operator.value)
|
|
||||||
.includes(aggregateOperator);
|
|
||||||
|
|
||||||
if (!isCurrentOperatorAvailableInList) {
|
|
||||||
handleChangeOperator(initialOperators[0].value);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
dataSource,
|
|
||||||
initialDataSource,
|
|
||||||
panelType,
|
|
||||||
operators,
|
|
||||||
aggregateOperator,
|
|
||||||
handleChangeOperator,
|
|
||||||
]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const additionalFilters = getNewListOfAdditionalFilters(dataSource);
|
const additionalFilters = getNewListOfAdditionalFilters(dataSource);
|
||||||
@ -156,6 +153,7 @@ export const useQueryOperations: UseQueryOperations = ({ query, index }) => {
|
|||||||
}, [dataSource, aggregateOperator, getNewListOfAdditionalFilters]);
|
}, [dataSource, aggregateOperator, getNewListOfAdditionalFilters]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
isTracePanelType,
|
||||||
isMetricsDataSource,
|
isMetricsDataSource,
|
||||||
operators,
|
operators,
|
||||||
listOfAdditionalFilters,
|
listOfAdditionalFilters,
|
||||||
|
@ -5,11 +5,9 @@ import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
|||||||
import { useGetCompositeQueryParam } from './useGetCompositeQueryParam';
|
import { useGetCompositeQueryParam } from './useGetCompositeQueryParam';
|
||||||
import { useQueryBuilder } from './useQueryBuilder';
|
import { useQueryBuilder } from './useQueryBuilder';
|
||||||
|
|
||||||
type UseShareBuilderUrlParams = { defaultValue: Query };
|
export type UseShareBuilderUrlParams = { defaultValue: Query };
|
||||||
|
|
||||||
export const useShareBuilderUrl = ({
|
export const useShareBuilderUrl = (defaultQuery: Query): void => {
|
||||||
defaultValue,
|
|
||||||
}: UseShareBuilderUrlParams): void => {
|
|
||||||
const { redirectWithQueryBuilderData, resetStagedQuery } = useQueryBuilder();
|
const { redirectWithQueryBuilderData, resetStagedQuery } = useQueryBuilder();
|
||||||
const urlQuery = useUrlQuery();
|
const urlQuery = useUrlQuery();
|
||||||
|
|
||||||
@ -17,9 +15,9 @@ export const useShareBuilderUrl = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!compositeQuery) {
|
if (!compositeQuery) {
|
||||||
redirectWithQueryBuilderData(defaultValue);
|
redirectWithQueryBuilderData(defaultQuery);
|
||||||
}
|
}
|
||||||
}, [defaultValue, urlQuery, redirectWithQueryBuilderData, compositeQuery]);
|
}, [defaultQuery, urlQuery, redirectWithQueryBuilderData, compositeQuery]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => (): void => {
|
() => (): 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 startUnix = start.valueOf();
|
||||||
const endUnix = end.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(
|
expect(
|
||||||
getStep({
|
getStep({
|
||||||
|
@ -7,7 +7,7 @@ export const getDashboardVariables = (): Record<string, unknown> => {
|
|||||||
globalTime,
|
globalTime,
|
||||||
dashboards: { dashboards },
|
dashboards: { dashboards },
|
||||||
} = store.getState();
|
} = store.getState();
|
||||||
const [selectedDashboard] = dashboards;
|
const [selectedDashboard] = dashboards || [];
|
||||||
const {
|
const {
|
||||||
data: { variables = {} },
|
data: { variables = {} },
|
||||||
} = selectedDashboard;
|
} = 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 DefaultStepSize = 60;
|
||||||
export const MaxDataPoints = 200;
|
export const MaxDataPoints = 300;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns relevant step size based on given start and end date.
|
* 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 endDate = dayjs(convertToMs(Number(end), inputFormat));
|
||||||
const diffSec = Math.abs(endDate.diff(startDate, 's'));
|
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;
|
export default getStep;
|
||||||
|
@ -35,5 +35,9 @@ export const convertNewDataToOld = (
|
|||||||
});
|
});
|
||||||
const oldResultType = resultType;
|
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