mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-09-14 03:23:14 +08:00
commit
206e8b8dc3
2
.github/workflows/e2e-k3s.yaml
vendored
2
.github/workflows/e2e-k3s.yaml
vendored
@ -37,7 +37,7 @@ jobs:
|
|||||||
kubectl create ns sample-application
|
kubectl create ns sample-application
|
||||||
|
|
||||||
# apply hotrod k8s manifest file
|
# apply hotrod k8s manifest file
|
||||||
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/main/sample-apps/hotrod/hotrod.yaml
|
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/develop/sample-apps/hotrod/hotrod.yaml
|
||||||
|
|
||||||
# wait for all deployments in sample-application namespace to be READY
|
# wait for all deployments in sample-application namespace to be READY
|
||||||
kubectl -n sample-application get deploy --output name | xargs -r -n1 -t kubectl -n sample-application rollout status --timeout=300s
|
kubectl -n sample-application get deploy --output name | xargs -r -n1 -t kubectl -n sample-application rollout status --timeout=300s
|
||||||
|
@ -338,7 +338,7 @@ to make SigNoz UI available at [localhost:3301](http://localhost:3301)
|
|||||||
**5.1.1 To install the HotROD sample app:**
|
**5.1.1 To install the HotROD sample app:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-install.sh \
|
curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-install.sh \
|
||||||
| HELM_RELEASE=my-release SIGNOZ_NAMESPACE=platform bash
|
| HELM_RELEASE=my-release SIGNOZ_NAMESPACE=platform bash
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -361,7 +361,7 @@ kubectl -n sample-application run strzal --image=djbingham/curl \
|
|||||||
**5.1.4 To delete the HotROD sample app:**
|
**5.1.4 To delete the HotROD sample app:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sL https://github.com/SigNoz/signoz/raw/main/sample-apps/hotrod/hotrod-delete.sh \
|
curl -sL https://github.com/SigNoz/signoz/raw/develop/sample-apps/hotrod/hotrod-delete.sh \
|
||||||
| HOTROD_NAMESPACE=sample-application bash
|
| HOTROD_NAMESPACE=sample-application bash
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ from the HotROD application, you should see the data generated from hotrod in Si
|
|||||||
```sh
|
```sh
|
||||||
kubectl create ns sample-application
|
kubectl create ns sample-application
|
||||||
|
|
||||||
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/main/sample-apps/hotrod/hotrod.yaml
|
kubectl -n sample-application apply -f https://raw.githubusercontent.com/SigNoz/signoz/develop/sample-apps/hotrod/hotrod.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
To generate load:
|
To generate load:
|
||||||
@ -66,7 +66,7 @@ To generate load:
|
|||||||
```sh
|
```sh
|
||||||
kubectl -n sample-application run strzal --image=djbingham/curl \
|
kubectl -n sample-application run strzal --image=djbingham/curl \
|
||||||
--restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \
|
--restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \
|
||||||
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm
|
'user_count=6' -F 'spawn_rate=2' http://locust-master:8089/swarm
|
||||||
```
|
```
|
||||||
|
|
||||||
To stop load:
|
To stop load:
|
||||||
|
@ -137,7 +137,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.22.0
|
image: signoz/query-service:0.23.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.22.0
|
image: signoz/frontend:0.23.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.2
|
image: signoz/signoz-otel-collector:0.79.3
|
||||||
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.2
|
image: signoz/signoz-otel-collector:0.79.3
|
||||||
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
|
||||||
|
@ -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.2
|
image: signoz/signoz-otel-collector:0.79.3
|
||||||
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.2
|
image: signoz/signoz-otel-collector:0.79.3
|
||||||
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
|
||||||
|
@ -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.22.0}
|
image: signoz/query-service:${DOCKER_TAG:-0.23.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.22.0}
|
image: signoz/frontend:${DOCKER_TAG:-0.23.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.2}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.3}
|
||||||
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.2}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.79.3}
|
||||||
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
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
type APIHandlerOptions struct {
|
type APIHandlerOptions struct {
|
||||||
DataConnector interfaces.DataConnector
|
DataConnector interfaces.DataConnector
|
||||||
SkipConfig *basemodel.SkipConfig
|
SkipConfig *basemodel.SkipConfig
|
||||||
|
PreferDelta bool
|
||||||
AppDao dao.ModelDao
|
AppDao dao.ModelDao
|
||||||
RulesManager *rules.Manager
|
RulesManager *rules.Manager
|
||||||
FeatureFlags baseint.FeatureLookup
|
FeatureFlags baseint.FeatureLookup
|
||||||
@ -34,6 +35,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,
|
SkipConfig: opts.SkipConfig,
|
||||||
|
PerferDelta: opts.PreferDelta,
|
||||||
AppDao: opts.AppDao,
|
AppDao: opts.AppDao,
|
||||||
RuleManager: opts.RulesManager,
|
RuleManager: opts.RulesManager,
|
||||||
FeatureFlags: opts.FeatureFlags})
|
FeatureFlags: opts.FeatureFlags})
|
||||||
|
@ -56,6 +56,7 @@ type ServerOptions struct {
|
|||||||
// alert specific params
|
// alert specific params
|
||||||
DisableRules bool
|
DisableRules bool
|
||||||
RuleRepoURL string
|
RuleRepoURL string
|
||||||
|
PreferDelta bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server runs HTTP api service
|
// Server runs HTTP api service
|
||||||
@ -170,6 +171,7 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) {
|
|||||||
apiOpts := api.APIHandlerOptions{
|
apiOpts := api.APIHandlerOptions{
|
||||||
DataConnector: reader,
|
DataConnector: reader,
|
||||||
SkipConfig: skipConfig,
|
SkipConfig: skipConfig,
|
||||||
|
PreferDelta: serverOptions.PreferDelta,
|
||||||
AppDao: modelDao,
|
AppDao: modelDao,
|
||||||
RulesManager: rm,
|
RulesManager: rm,
|
||||||
FeatureFlags: lm,
|
FeatureFlags: lm,
|
||||||
|
@ -83,10 +83,12 @@ func main() {
|
|||||||
var ruleRepoURL string
|
var ruleRepoURL string
|
||||||
|
|
||||||
var enableQueryServiceLogOTLPExport bool
|
var enableQueryServiceLogOTLPExport bool
|
||||||
|
var preferDelta 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.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.BoolVar(&preferDelta, "prefer-delta", false, "(prefer delta over raw metrics)")
|
||||||
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)")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
@ -102,6 +104,7 @@ func main() {
|
|||||||
HTTPHostPort: baseconst.HTTPHostPort,
|
HTTPHostPort: baseconst.HTTPHostPort,
|
||||||
PromConfigPath: promConfigPath,
|
PromConfigPath: promConfigPath,
|
||||||
SkipTopLvlOpsPath: skipTopLvlOpsPath,
|
SkipTopLvlOpsPath: skipTopLvlOpsPath,
|
||||||
|
PreferDelta: preferDelta,
|
||||||
PrivateHostPort: baseconst.PrivateHostPort,
|
PrivateHostPort: baseconst.PrivateHostPort,
|
||||||
DisableRules: disableRules,
|
DisableRules: disableRules,
|
||||||
RuleRepoURL: ruleRepoURL,
|
RuleRepoURL: ruleRepoURL,
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"options_menu": {
|
"options_menu": {
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
"format": "Format",
|
"format": "Format",
|
||||||
"row": "Row",
|
"raw": "Raw",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"column": "Column",
|
"column": "Column",
|
||||||
"maxLines": "Max lines per Row",
|
"maxLines": "Max lines per Row",
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"options_menu": {
|
"options_menu": {
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
"format": "Format",
|
"format": "Format",
|
||||||
"row": "Row",
|
"raw": "Raw",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"column": "Column",
|
"column": "Column",
|
||||||
"maxLines": "Max lines per Row",
|
"maxLines": "Max lines per Row",
|
||||||
|
@ -7,7 +7,7 @@ export const ServicesTablePage = Loadable(
|
|||||||
export const ServiceMetricsPage = Loadable(
|
export const ServiceMetricsPage = Loadable(
|
||||||
() =>
|
() =>
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "ServiceMetricsPage" */ 'pages/MetricApplication'
|
/* webpackChunkName: "ServiceMetricsPage" */ 'pages/MetricsApplication'
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { PayloadProps, Props } from 'types/api/metrics/getServiceOverview';
|
import { PayloadProps, Props } from 'types/api/metrics/getServiceOverview';
|
||||||
|
|
||||||
const getServiceOverview = async (
|
const getServiceOverview = async (props: Props): Promise<PayloadProps> => {
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`/service/overview`, {
|
const response = await axios.post(`/service/overview`, {
|
||||||
start: `${props.start}`,
|
start: `${props.start}`,
|
||||||
end: `${props.end}`,
|
end: `${props.end}`,
|
||||||
@ -16,15 +10,7 @@ const getServiceOverview = async (
|
|||||||
tags: props.selectedTags,
|
tags: props.selectedTags,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return response.data;
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getServiceOverview;
|
export default getServiceOverview;
|
||||||
|
@ -1,24 +1,12 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { PayloadProps, Props } from 'types/api/metrics/getTopLevelOperations';
|
|
||||||
|
|
||||||
const getTopLevelOperations = async (
|
const getTopLevelOperations = async (): Promise<ServiceDataProps> => {
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`/service/top_level_operations`);
|
const response = await axios.post(`/service/top_level_operations`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
export type ServiceDataProps = {
|
||||||
statusCode: 200,
|
[serviceName: string]: string[];
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data[props.service],
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getTopLevelOperations;
|
export default getTopLevelOperations;
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
import axios from 'api';
|
import axios from 'api';
|
||||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
|
||||||
import { PayloadProps, Props } from 'types/api/metrics/getTopOperations';
|
import { PayloadProps, Props } from 'types/api/metrics/getTopOperations';
|
||||||
|
|
||||||
const getTopOperations = async (
|
const getTopOperations = async (props: Props): Promise<PayloadProps> => {
|
||||||
props: Props,
|
|
||||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`/service/top_operations`, {
|
const response = await axios.post(`/service/top_operations`, {
|
||||||
start: `${props.start}`,
|
start: `${props.start}`,
|
||||||
end: `${props.end}`,
|
end: `${props.end}`,
|
||||||
@ -15,15 +9,7 @@ const getTopOperations = async (
|
|||||||
tags: props.selectedTags,
|
tags: props.selectedTags,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return response.data;
|
||||||
statusCode: 200,
|
|
||||||
error: null,
|
|
||||||
message: response.data.status,
|
|
||||||
payload: response.data,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return ErrorResponseHandler(error as AxiosError);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getTopOperations;
|
export default getTopOperations;
|
||||||
|
27
frontend/src/components/ExplorerCard/index.tsx
Normal file
27
frontend/src/components/ExplorerCard/index.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Card, Space, Typography } from 'antd';
|
||||||
|
import TextToolTip from 'components/TextToolTip';
|
||||||
|
|
||||||
|
function ExplorerCard({ children }: Props): JSX.Element {
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
size="small"
|
||||||
|
title={
|
||||||
|
<Space>
|
||||||
|
<Typography>Query Builder</Typography>
|
||||||
|
<TextToolTip
|
||||||
|
url="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=new-query-builder"
|
||||||
|
text="More details on how to use query builder"
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExplorerCard;
|
@ -0,0 +1,9 @@
|
|||||||
|
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
||||||
|
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
|
export type LogDetailProps = {
|
||||||
|
log: ILog | null;
|
||||||
|
onClose: () => void;
|
||||||
|
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||||
|
Pick<ActionItemProps, 'onClickActionItem'>;
|
52
frontend/src/components/LogDetail/index.tsx
Normal file
52
frontend/src/components/LogDetail/index.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { Drawer, Tabs } from 'antd';
|
||||||
|
import JSONView from 'container/LogDetailedView/JsonView';
|
||||||
|
import TableView from 'container/LogDetailedView/TableView';
|
||||||
|
|
||||||
|
import { LogDetailProps } from './LogDetail.interfaces';
|
||||||
|
|
||||||
|
function LogDetail({
|
||||||
|
log,
|
||||||
|
onClose,
|
||||||
|
onAddToQuery,
|
||||||
|
onClickActionItem,
|
||||||
|
}: LogDetailProps): JSX.Element {
|
||||||
|
const onDrawerClose = (): void => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: 'Table',
|
||||||
|
key: '1',
|
||||||
|
children: log && (
|
||||||
|
<TableView
|
||||||
|
logData={log}
|
||||||
|
onAddToQuery={onAddToQuery}
|
||||||
|
onClickActionItem={onClickActionItem}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'JSON',
|
||||||
|
key: '2',
|
||||||
|
children: log && <JSONView logData={log} />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
width="60%"
|
||||||
|
title="Log Details"
|
||||||
|
placement="right"
|
||||||
|
closable
|
||||||
|
onClose={onDrawerClose}
|
||||||
|
open={log !== null}
|
||||||
|
style={{ overscrollBehavior: 'contain' }}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Tabs defaultActiveKey="1" items={items} />
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LogDetail;
|
@ -1,39 +1,18 @@
|
|||||||
import { Popover } from 'antd';
|
import { Popover } from 'antd';
|
||||||
import ROUTES from 'constants/routes';
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
import history from 'lib/history';
|
|
||||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
|
||||||
import { memo, ReactNode, useCallback, useMemo } from 'react';
|
import { memo, ReactNode, useCallback, useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { ILogsReducer } from 'types/reducer/logs';
|
|
||||||
|
|
||||||
import { ButtonContainer } from './styles';
|
import { ButtonContainer } from './styles';
|
||||||
|
|
||||||
function AddToQueryHOC({
|
function AddToQueryHOC({
|
||||||
fieldKey,
|
fieldKey,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
|
onAddToQuery,
|
||||||
children,
|
children,
|
||||||
}: AddToQueryHOCProps): JSX.Element {
|
}: AddToQueryHOCProps): JSX.Element {
|
||||||
const {
|
|
||||||
searchFilter: { queryString },
|
|
||||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
|
||||||
|
|
||||||
const generatedQuery = useMemo(
|
|
||||||
() => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }),
|
|
||||||
[fieldKey, fieldValue],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleQueryAdd = useCallback(() => {
|
const handleQueryAdd = useCallback(() => {
|
||||||
let updatedQueryString = queryString || '';
|
onAddToQuery(fieldKey, fieldValue, OPERATORS.IN);
|
||||||
|
}, [fieldKey, fieldValue, onAddToQuery]);
|
||||||
if (updatedQueryString.length === 0) {
|
|
||||||
updatedQueryString += `${generatedQuery}`;
|
|
||||||
} else {
|
|
||||||
updatedQueryString += ` AND ${generatedQuery}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
|
|
||||||
}, [generatedQuery, queryString]);
|
|
||||||
|
|
||||||
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
|
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
|
||||||
fieldKey,
|
fieldKey,
|
||||||
@ -48,9 +27,10 @@ function AddToQueryHOC({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddToQueryHOCProps {
|
export interface AddToQueryHOCProps {
|
||||||
fieldKey: string;
|
fieldKey: string;
|
||||||
fieldValue: string;
|
fieldValue: string;
|
||||||
|
onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,16 +8,13 @@ import { useNotifications } from 'hooks/useNotifications';
|
|||||||
// utils
|
// utils
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { useCopyToClipboard } from 'react-use';
|
import { useCopyToClipboard } from 'react-use';
|
||||||
// interfaces
|
// interfaces
|
||||||
import { AppState } from 'store/reducers';
|
import { IField } from 'types/api/logs/fields';
|
||||||
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
import { ILogsReducer } from 'types/reducer/logs';
|
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import AddToQueryHOC from '../AddToQueryHOC';
|
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
|
||||||
import CopyClipboardHOC from '../CopyClipboardHOC';
|
import CopyClipboardHOC from '../CopyClipboardHOC';
|
||||||
// styles
|
// styles
|
||||||
import {
|
import {
|
||||||
@ -36,6 +33,10 @@ interface LogFieldProps {
|
|||||||
fieldKey: string;
|
fieldKey: string;
|
||||||
fieldValue: string;
|
fieldValue: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LogSelectedFieldProps = LogFieldProps &
|
||||||
|
Pick<AddToQueryHOCProps, 'onAddToQuery'>;
|
||||||
|
|
||||||
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
|
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
|
||||||
const html = useMemo(
|
const html = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@ -59,10 +60,15 @@ function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
|
|||||||
function LogSelectedField({
|
function LogSelectedField({
|
||||||
fieldKey = '',
|
fieldKey = '',
|
||||||
fieldValue = '',
|
fieldValue = '',
|
||||||
}: LogFieldProps): JSX.Element {
|
onAddToQuery,
|
||||||
|
}: LogSelectedFieldProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<SelectedLog>
|
<SelectedLog>
|
||||||
<AddToQueryHOC fieldKey={fieldKey} fieldValue={fieldValue}>
|
<AddToQueryHOC
|
||||||
|
fieldKey={fieldKey}
|
||||||
|
fieldValue={fieldValue}
|
||||||
|
onAddToQuery={onAddToQuery}
|
||||||
|
>
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
<span style={{ color: blue[4] }}>{fieldKey}</span>
|
<span style={{ color: blue[4] }}>{fieldKey}</span>
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
@ -77,26 +83,26 @@ function LogSelectedField({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListLogViewProps {
|
type ListLogViewProps = {
|
||||||
logData: ILog;
|
logData: ILog;
|
||||||
}
|
onOpenDetailedView: (log: ILog) => void;
|
||||||
function ListLogView({ logData }: ListLogViewProps): JSX.Element {
|
selectedFields: IField[];
|
||||||
const {
|
} & Pick<AddToQueryHOCProps, 'onAddToQuery'>;
|
||||||
fields: { selected },
|
|
||||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
function ListLogView({
|
||||||
|
logData,
|
||||||
|
selectedFields,
|
||||||
|
onOpenDetailedView,
|
||||||
|
onAddToQuery,
|
||||||
|
}: ListLogViewProps): JSX.Element {
|
||||||
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
||||||
|
|
||||||
const [, setCopy] = useCopyToClipboard();
|
const [, setCopy] = useCopyToClipboard();
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
const handleDetailedView = useCallback(() => {
|
const handleDetailedView = useCallback(() => {
|
||||||
dispatch({
|
onOpenDetailedView(logData);
|
||||||
type: SET_DETAILED_LOG_DATA,
|
}, [logData, onOpenDetailedView]);
|
||||||
payload: logData,
|
|
||||||
});
|
|
||||||
}, [dispatch, logData]);
|
|
||||||
|
|
||||||
const handleCopyJSON = (): void => {
|
const handleCopyJSON = (): void => {
|
||||||
setCopy(JSON.stringify(logData, null, 2));
|
setCopy(JSON.stringify(logData, null, 2));
|
||||||
@ -106,8 +112,16 @@ function ListLogView({ logData }: ListLogViewProps): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updatedSelecedFields = useMemo(
|
const updatedSelecedFields = useMemo(
|
||||||
() => selected.filter((e) => e.name !== 'id'),
|
() => selectedFields.filter((e) => e.name !== 'id'),
|
||||||
[selected],
|
[selectedFields],
|
||||||
|
);
|
||||||
|
|
||||||
|
const timestampValue = useMemo(
|
||||||
|
() =>
|
||||||
|
typeof flattenLogData.timestamp === 'string'
|
||||||
|
? dayjs(flattenLogData.timestamp).format()
|
||||||
|
: dayjs(flattenLogData.timestamp / 1e6).format(),
|
||||||
|
[flattenLogData.timestamp],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -119,10 +133,7 @@ function ListLogView({ logData }: ListLogViewProps): JSX.Element {
|
|||||||
{flattenLogData.stream && (
|
{flattenLogData.stream && (
|
||||||
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
|
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
|
||||||
)}
|
)}
|
||||||
<LogGeneralField
|
<LogGeneralField fieldKey="timestamp" fieldValue={timestampValue} />
|
||||||
fieldKey="timestamp"
|
|
||||||
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
</LogContainer>
|
</LogContainer>
|
||||||
<div>
|
<div>
|
||||||
@ -132,6 +143,7 @@ function ListLogView({ logData }: ListLogViewProps): JSX.Element {
|
|||||||
key={field.name}
|
key={field.name}
|
||||||
fieldKey={field.name}
|
fieldKey={field.name}
|
||||||
fieldValue={flattenLogData[field.name] as never}
|
fieldValue={flattenLogData[field.name] as never}
|
||||||
|
onAddToQuery={onAddToQuery}
|
||||||
/>
|
/>
|
||||||
) : null,
|
) : null,
|
||||||
)}
|
)}
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
import { Card, Typography } from 'antd';
|
import { Card, Typography } from 'antd';
|
||||||
import styled, { keyframes } from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const fadeInAnimation = keyframes`
|
|
||||||
0% { opacity: 0; }
|
|
||||||
100% { opacity: 1;}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const Container = styled(Card)`
|
export const Container = styled(Card)`
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
@ -12,9 +7,6 @@ export const Container = styled(Card)`
|
|||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
padding: 0.3rem 0.6rem;
|
padding: 0.3rem 0.6rem;
|
||||||
}
|
}
|
||||||
animation-name: ${fadeInAnimation};
|
|
||||||
animation-duration: 0.2s;
|
|
||||||
animation-timing-function: ease-in;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const Text = styled(Typography.Text)`
|
export const Text = styled(Typography.Text)`
|
||||||
|
@ -30,7 +30,10 @@ function RawLogView(props: RawLogViewProps): JSX.Element {
|
|||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const text = useMemo(
|
const text = useMemo(
|
||||||
() => `${dayjs(data.timestamp / 1e6).format()} | ${data.body}`,
|
() =>
|
||||||
|
typeof data.timestamp === 'string'
|
||||||
|
? `${dayjs(data.timestamp).format()} | ${data.body}`
|
||||||
|
: `${dayjs(data.timestamp / 1e6).format()} | ${data.body}`,
|
||||||
[data.timestamp, data.body],
|
[data.timestamp, data.body],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,121 +1,18 @@
|
|||||||
import { ExpandAltOutlined } from '@ant-design/icons';
|
import { Table } from 'antd';
|
||||||
import Convert from 'ansi-to-html';
|
|
||||||
import { Table, Typography } from 'antd';
|
|
||||||
import { ColumnsType, ColumnType } from 'antd/es/table';
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import dompurify from 'dompurify';
|
|
||||||
// utils
|
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { IField } from 'types/api/logs/fields';
|
|
||||||
// interfaces
|
|
||||||
import { ILog } from 'types/api/logs/log';
|
|
||||||
|
|
||||||
// styles
|
|
||||||
import { ExpandIconWrapper } from '../RawLogView/styles';
|
|
||||||
// config
|
// config
|
||||||
import { defaultCellStyle, defaultTableStyle, tableScroll } from './config';
|
import { tableScroll } from './config';
|
||||||
import { TableBodyContent } from './styles';
|
import { LogsTableViewProps } from './types';
|
||||||
|
import { useTableView } from './useTableView';
|
||||||
type ColumnTypeRender<T = unknown> = ReturnType<
|
|
||||||
NonNullable<ColumnType<T>['render']>
|
|
||||||
>;
|
|
||||||
|
|
||||||
type LogsTableViewProps = {
|
|
||||||
logs: ILog[];
|
|
||||||
fields: IField[];
|
|
||||||
linesPerRow: number;
|
|
||||||
onClickExpand: (log: ILog) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const convert = new Convert();
|
|
||||||
|
|
||||||
function LogsTableView(props: LogsTableViewProps): JSX.Element {
|
function LogsTableView(props: LogsTableViewProps): JSX.Element {
|
||||||
const { logs, fields, linesPerRow, onClickExpand } = props;
|
const { dataSource, columns } = useTableView(props);
|
||||||
|
|
||||||
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
|
|
||||||
logs,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
|
||||||
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
|
||||||
.filter((e) => e.name !== 'id')
|
|
||||||
.map(({ name }) => ({
|
|
||||||
title: name,
|
|
||||||
dataIndex: name,
|
|
||||||
key: name,
|
|
||||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
|
||||||
props: {
|
|
||||||
style: defaultCellStyle,
|
|
||||||
},
|
|
||||||
children: (
|
|
||||||
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
|
|
||||||
{field}
|
|
||||||
</Typography.Paragraph>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
dataIndex: 'id',
|
|
||||||
key: 'expand',
|
|
||||||
// https://github.com/ant-design/ant-design/discussions/36886
|
|
||||||
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
|
|
||||||
props: {
|
|
||||||
style: defaultCellStyle,
|
|
||||||
},
|
|
||||||
children: (
|
|
||||||
<ExpandIconWrapper
|
|
||||||
onClick={(): void => {
|
|
||||||
onClickExpand((item as unknown) as ILog);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ExpandAltOutlined />
|
|
||||||
</ExpandIconWrapper>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'timestamp',
|
|
||||||
dataIndex: 'timestamp',
|
|
||||||
key: 'timestamp',
|
|
||||||
// https://github.com/ant-design/ant-design/discussions/36886
|
|
||||||
render: (field): ColumnTypeRender<Record<string, unknown>> => {
|
|
||||||
const date = dayjs(field / 1e6).format();
|
|
||||||
return {
|
|
||||||
children: <Typography.Paragraph ellipsis>{date}</Typography.Paragraph>,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...fieldColumns,
|
|
||||||
{
|
|
||||||
title: 'body',
|
|
||||||
dataIndex: 'body',
|
|
||||||
key: 'body',
|
|
||||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
|
||||||
props: {
|
|
||||||
style: defaultTableStyle,
|
|
||||||
},
|
|
||||||
children: (
|
|
||||||
<TableBodyContent
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: convert.toHtml(dompurify.sanitize(field)),
|
|
||||||
}}
|
|
||||||
linesPerRow={linesPerRow}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}, [fields, linesPerRow, onClickExpand]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
size="small"
|
size="small"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={flattenLogData}
|
dataSource={dataSource}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
bordered
|
bordered
|
||||||
|
23
frontend/src/components/Logs/TableView/types.ts
Normal file
23
frontend/src/components/Logs/TableView/types.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { ColumnsType, ColumnType } from 'antd/es/table';
|
||||||
|
import { IField } from 'types/api/logs/fields';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
|
export type ColumnTypeRender<T = unknown> = ReturnType<
|
||||||
|
NonNullable<ColumnType<T>['render']>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type LogsTableViewProps = {
|
||||||
|
logs: ILog[];
|
||||||
|
fields: IField[];
|
||||||
|
linesPerRow: number;
|
||||||
|
onClickExpand: (log: ILog) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseTableViewResult = {
|
||||||
|
columns: ColumnsType<Record<string, unknown>>;
|
||||||
|
dataSource: Record<string, string>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UseTableViewProps = {
|
||||||
|
appendTo?: 'center' | 'end';
|
||||||
|
} & LogsTableViewProps;
|
114
frontend/src/components/Logs/TableView/useTableView.tsx
Normal file
114
frontend/src/components/Logs/TableView/useTableView.tsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import { ExpandAltOutlined } from '@ant-design/icons';
|
||||||
|
import Convert from 'ansi-to-html';
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import { ColumnsType } from 'antd/es/table';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import dompurify from 'dompurify';
|
||||||
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
|
import { ExpandIconWrapper } from '../RawLogView/styles';
|
||||||
|
import { defaultCellStyle, defaultTableStyle } from './config';
|
||||||
|
import { TableBodyContent } from './styles';
|
||||||
|
import {
|
||||||
|
ColumnTypeRender,
|
||||||
|
UseTableViewProps,
|
||||||
|
UseTableViewResult,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const convert = new Convert();
|
||||||
|
|
||||||
|
export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||||
|
const {
|
||||||
|
logs,
|
||||||
|
fields,
|
||||||
|
linesPerRow,
|
||||||
|
onClickExpand,
|
||||||
|
appendTo = 'center',
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
|
||||||
|
logs,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
||||||
|
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
||||||
|
.filter((e) => e.name !== 'id')
|
||||||
|
.map(({ name }) => ({
|
||||||
|
title: name,
|
||||||
|
dataIndex: name,
|
||||||
|
key: name,
|
||||||
|
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||||
|
props: {
|
||||||
|
style: defaultCellStyle,
|
||||||
|
},
|
||||||
|
children: (
|
||||||
|
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
|
||||||
|
{field}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'expand',
|
||||||
|
// https://github.com/ant-design/ant-design/discussions/36886
|
||||||
|
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
|
||||||
|
props: {
|
||||||
|
style: defaultCellStyle,
|
||||||
|
},
|
||||||
|
children: (
|
||||||
|
<ExpandIconWrapper
|
||||||
|
onClick={(): void => {
|
||||||
|
onClickExpand((item as unknown) as ILog);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExpandAltOutlined />
|
||||||
|
</ExpandIconWrapper>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'timestamp',
|
||||||
|
dataIndex: 'timestamp',
|
||||||
|
key: 'timestamp',
|
||||||
|
// https://github.com/ant-design/ant-design/discussions/36886
|
||||||
|
render: (field): ColumnTypeRender<Record<string, unknown>> => {
|
||||||
|
const date =
|
||||||
|
typeof field === 'string'
|
||||||
|
? dayjs(field).format()
|
||||||
|
: dayjs(field / 1e6).format();
|
||||||
|
return {
|
||||||
|
children: <Typography.Paragraph ellipsis>{date}</Typography.Paragraph>,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...(appendTo === 'center' ? fieldColumns : []),
|
||||||
|
{
|
||||||
|
title: 'body',
|
||||||
|
dataIndex: 'body',
|
||||||
|
key: 'body',
|
||||||
|
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||||
|
props: {
|
||||||
|
style: defaultTableStyle,
|
||||||
|
},
|
||||||
|
children: (
|
||||||
|
<TableBodyContent
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: convert.toHtml(dompurify.sanitize(field)),
|
||||||
|
}}
|
||||||
|
linesPerRow={linesPerRow}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
...(appendTo === 'end' ? fieldColumns : []),
|
||||||
|
];
|
||||||
|
}, [fields, linesPerRow, appendTo, onClickExpand]);
|
||||||
|
|
||||||
|
return { columns, dataSource: flattenLogData };
|
||||||
|
};
|
5
frontend/src/components/TabLabel/TabLabel.interfaces.ts
Normal file
5
frontend/src/components/TabLabel/TabLabel.interfaces.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type TabLabelProps = {
|
||||||
|
isDisabled: boolean;
|
||||||
|
label: string;
|
||||||
|
tooltipText?: string;
|
||||||
|
};
|
29
frontend/src/components/TabLabel/index.tsx
Normal file
29
frontend/src/components/TabLabel/index.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Tooltip } from 'antd';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
import { TabLabelProps } from './TabLabel.interfaces';
|
||||||
|
|
||||||
|
function TabLabel({
|
||||||
|
label,
|
||||||
|
isDisabled,
|
||||||
|
tooltipText,
|
||||||
|
}: TabLabelProps): JSX.Element {
|
||||||
|
const currentLabel = <span>{label}</span>;
|
||||||
|
|
||||||
|
if (isDisabled) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
trigger="hover"
|
||||||
|
autoAdjustOverflow
|
||||||
|
placement="top"
|
||||||
|
title={tooltipText}
|
||||||
|
>
|
||||||
|
{currentLabel}
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(TabLabel);
|
@ -1 +1 @@
|
|||||||
export const style = { fontSize: '1.3125rem' };
|
export const style = { fontSize: '1rem' };
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
const SOMETHING_WENT_WRONG = 'Something went wrong';
|
||||||
|
|
||||||
const getVersion = 'version';
|
const getVersion = 'version';
|
||||||
|
|
||||||
export { getVersion };
|
export { getVersion, SOMETHING_WENT_WRONG };
|
||||||
|
@ -6,4 +6,6 @@ export enum LOCALSTORAGE {
|
|||||||
THEME = 'THEME',
|
THEME = 'THEME',
|
||||||
LOGS_VIEW_MODE = 'LOGS_VIEW_MODE',
|
LOGS_VIEW_MODE = 'LOGS_VIEW_MODE',
|
||||||
LOGS_LINES_PER_ROW = 'LOGS_LINES_PER_ROW',
|
LOGS_LINES_PER_ROW = 'LOGS_LINES_PER_ROW',
|
||||||
|
LOGS_LIST_OPTIONS = 'LOGS_LIST_OPTIONS',
|
||||||
|
TRACES_LIST_OPTIONS = 'TRACES_LIST_OPTIONS',
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
|||||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||||
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
||||||
import {
|
import {
|
||||||
|
AutocompleteType,
|
||||||
BaseAutocompleteData,
|
BaseAutocompleteData,
|
||||||
LocalDataType,
|
LocalDataType,
|
||||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
@ -14,6 +15,7 @@ import {
|
|||||||
IPromQLQuery,
|
IPromQLQuery,
|
||||||
Query,
|
Query,
|
||||||
QueryState,
|
QueryState,
|
||||||
|
TagFilter,
|
||||||
} from 'types/api/queryBuilder/queryBuilderData';
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import {
|
import {
|
||||||
@ -49,6 +51,11 @@ export const baseAutoCompleteIdKeysOrder: (keyof Omit<
|
|||||||
'id'
|
'id'
|
||||||
>)[] = ['key', 'dataType', 'type', 'isColumn'];
|
>)[] = ['key', 'dataType', 'type', 'isColumn'];
|
||||||
|
|
||||||
|
export const autocompleteType: Record<AutocompleteType, AutocompleteType> = {
|
||||||
|
resource: 'resource',
|
||||||
|
tag: 'tag',
|
||||||
|
};
|
||||||
|
|
||||||
export const formulasNames: string[] = Array.from(
|
export const formulasNames: string[] = Array.from(
|
||||||
Array(MAX_FORMULAS),
|
Array(MAX_FORMULAS),
|
||||||
(_, i) => `F${i + 1}`,
|
(_, i) => `F${i + 1}`,
|
||||||
@ -59,7 +66,6 @@ export const alphabet: string[] = alpha.map((str) => String.fromCharCode(str));
|
|||||||
export enum QueryBuilderKeys {
|
export enum QueryBuilderKeys {
|
||||||
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
|
GET_AGGREGATE_ATTRIBUTE = 'GET_AGGREGATE_ATTRIBUTE',
|
||||||
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
|
GET_AGGREGATE_KEYS = 'GET_AGGREGATE_KEYS',
|
||||||
GET_ATTRIBUTE_KEY = 'GET_ATTRIBUTE_KEY',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapOfOperators = {
|
export const mapOfOperators = {
|
||||||
@ -113,6 +119,11 @@ export const initialAutocompleteData: BaseAutocompleteData = {
|
|||||||
type: null,
|
type: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const initialFilters: TagFilter = {
|
||||||
|
items: [],
|
||||||
|
op: 'AND',
|
||||||
|
};
|
||||||
|
|
||||||
const initialQueryBuilderFormValues: IBuilderQuery = {
|
const initialQueryBuilderFormValues: IBuilderQuery = {
|
||||||
dataSource: DataSource.METRICS,
|
dataSource: DataSource.METRICS,
|
||||||
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
|
queryName: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
|
||||||
|
1
frontend/src/constants/queryBuilderFilterConfig.ts
Normal file
1
frontend/src/constants/queryBuilderFilterConfig.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const DEBOUNCE_DELAY = 200;
|
@ -1,2 +1,16 @@
|
|||||||
export const COMPOSITE_QUERY = 'compositeQuery';
|
type QueryParamNames =
|
||||||
export const PANEL_TYPES_QUERY = 'panelTypes';
|
| 'compositeQuery'
|
||||||
|
| 'panelTypes'
|
||||||
|
| 'pageSize'
|
||||||
|
| 'viewMode'
|
||||||
|
| 'selectedFields'
|
||||||
|
| 'linesPerRow';
|
||||||
|
|
||||||
|
export const queryParamNamesMap: Record<QueryParamNames, QueryParamNames> = {
|
||||||
|
compositeQuery: 'compositeQuery',
|
||||||
|
panelTypes: 'panelTypes',
|
||||||
|
pageSize: 'pageSize',
|
||||||
|
viewMode: 'viewMode',
|
||||||
|
selectedFields: 'selectedFields',
|
||||||
|
linesPerRow: 'linesPerRow',
|
||||||
|
};
|
||||||
|
@ -3,3 +3,5 @@ export const FORMULA_REGEXP = /F\d+/;
|
|||||||
export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/;
|
export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/;
|
||||||
|
|
||||||
export const TYPE_ADDON_REGEXP = /_(.+)/;
|
export const TYPE_ADDON_REGEXP = /_(.+)/;
|
||||||
|
|
||||||
|
export const SPLIT_FIRST_UNDERSCORE = /(?<!^)_/;
|
||||||
|
@ -2,6 +2,8 @@ import { CSSProperties } from 'react';
|
|||||||
|
|
||||||
export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200];
|
export const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200];
|
||||||
|
|
||||||
|
export const DEFAULT_PER_PAGE_VALUE = 100;
|
||||||
|
|
||||||
export const defaultSelectStyle: CSSProperties = {
|
export const defaultSelectStyle: CSSProperties = {
|
||||||
minWidth: '6rem',
|
minWidth: '6rem',
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||||
|
|
||||||
|
export type ExplorerControlPanelProps = {
|
||||||
|
isShowPageSize: boolean;
|
||||||
|
isLoading: boolean;
|
||||||
|
optionsMenuConfig?: OptionsMenuConfig;
|
||||||
|
};
|
29
frontend/src/container/ExplorerControlPanel/index.tsx
Normal file
29
frontend/src/container/ExplorerControlPanel/index.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Col, Row } from 'antd';
|
||||||
|
import OptionsMenu from 'container/OptionsMenu';
|
||||||
|
import PageSizeSelect from 'container/PageSizeSelect';
|
||||||
|
|
||||||
|
import { ExplorerControlPanelProps } from './ExplorerControlPanel.interfaces';
|
||||||
|
import { ContainerStyled } from './styles';
|
||||||
|
|
||||||
|
function ExplorerControlPanel({
|
||||||
|
isLoading,
|
||||||
|
isShowPageSize,
|
||||||
|
optionsMenuConfig,
|
||||||
|
}: ExplorerControlPanelProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<ContainerStyled>
|
||||||
|
<Row justify="end" gutter={30}>
|
||||||
|
{optionsMenuConfig && (
|
||||||
|
<Col>
|
||||||
|
<OptionsMenu config={optionsMenuConfig} />
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
|
<Col>
|
||||||
|
<PageSizeSelect isLoading={isLoading} isShow={isShowPageSize} />
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</ContainerStyled>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ExplorerControlPanel;
|
5
frontend/src/container/ExplorerControlPanel/styles.ts
Normal file
5
frontend/src/container/ExplorerControlPanel/styles.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const ContainerStyled = styled.div`
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
`;
|
@ -1,8 +1,7 @@
|
|||||||
import { Button, Typography } from 'antd';
|
import { Button, Typography } from 'antd';
|
||||||
import createDashboard from 'api/dashboard/create';
|
import createDashboard from 'api/dashboard/create';
|
||||||
import axios from 'axios';
|
|
||||||
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
import { useGetAllDashboard } from 'hooks/dashboard/useGetAllDashboard';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import useAxiosError from 'hooks/useAxiosError';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMutation } from 'react-query';
|
import { useMutation } from 'react-query';
|
||||||
@ -15,10 +14,9 @@ import {
|
|||||||
Title,
|
Title,
|
||||||
Wrapper,
|
Wrapper,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
import { getSelectOptions } from './utils';
|
import { filterOptions, getSelectOptions } from './utils';
|
||||||
|
|
||||||
function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
|
function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
|
||||||
const { notifications } = useNotifications();
|
|
||||||
const { t } = useTranslation(['dashboard']);
|
const { t } = useTranslation(['dashboard']);
|
||||||
|
|
||||||
const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>(
|
const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>(
|
||||||
@ -31,21 +29,19 @@ function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
|
|||||||
refetch,
|
refetch,
|
||||||
} = useGetAllDashboard();
|
} = useGetAllDashboard();
|
||||||
|
|
||||||
|
const handleError = useAxiosError();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutate: createNewDashboard,
|
mutate: createNewDashboard,
|
||||||
isLoading: createDashboardLoading,
|
isLoading: createDashboardLoading,
|
||||||
} = useMutation(createDashboard, {
|
} = useMutation(createDashboard, {
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
onExport(data?.payload || null);
|
if (data.payload) {
|
||||||
|
onExport(data?.payload);
|
||||||
|
}
|
||||||
refetch();
|
refetch();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: handleError,
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
notifications.error({
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = useMemo(() => getSelectOptions(data?.payload || []), [data]);
|
const options = useMemo(() => getSelectOptions(data?.payload || []), [data]);
|
||||||
@ -90,10 +86,12 @@ function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
|
|||||||
<DashboardSelect
|
<DashboardSelect
|
||||||
placeholder="Select Dashboard"
|
placeholder="Select Dashboard"
|
||||||
options={options}
|
options={options}
|
||||||
|
showSearch
|
||||||
loading={isDashboardLoading}
|
loading={isDashboardLoading}
|
||||||
disabled={isDashboardLoading}
|
disabled={isDashboardLoading}
|
||||||
value={selectedDashboardId}
|
value={selectedDashboardId}
|
||||||
onSelect={handleSelect}
|
onSelect={handleSelect}
|
||||||
|
filterOption={filterOptions}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
export const MENU_KEY = {
|
|
||||||
EXPORT: 'export',
|
|
||||||
CREATE_ALERTS: 'create-alerts',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MENU_LABEL = {
|
|
||||||
EXPORT: 'Export Panel',
|
|
||||||
CREATE_ALERTS: 'Create Alerts',
|
|
||||||
};
|
|
@ -1,12 +1,12 @@
|
|||||||
import { Button, Dropdown, MenuProps, Modal } from 'antd';
|
import { AlertOutlined, AreaChartOutlined } from '@ant-design/icons';
|
||||||
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
|
import { Button, Modal, Space } from 'antd';
|
||||||
|
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, 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 { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
import { MENU_KEY, MENU_LABEL } from './config';
|
|
||||||
import ExportPanelContainer from './ExportPanel';
|
import ExportPanelContainer from './ExportPanel';
|
||||||
|
|
||||||
function ExportPanel({
|
function ExportPanel({
|
||||||
@ -22,57 +22,43 @@ function ExportPanel({
|
|||||||
|
|
||||||
const onCreateAlertsHandler = useCallback(() => {
|
const onCreateAlertsHandler = useCallback(() => {
|
||||||
history.push(
|
history.push(
|
||||||
`${ROUTES.ALERTS_NEW}?${COMPOSITE_QUERY}=${encodeURIComponent(
|
`${ROUTES.ALERTS_NEW}?${
|
||||||
JSON.stringify(query),
|
queryParamNamesMap.compositeQuery
|
||||||
)}`,
|
}=${encodeURIComponent(JSON.stringify(query))}`,
|
||||||
);
|
);
|
||||||
}, [query]);
|
}, [query]);
|
||||||
|
|
||||||
const onMenuClickHandler: MenuProps['onClick'] = useCallback(
|
|
||||||
(e: OnClickProps) => {
|
|
||||||
if (e.key === MENU_KEY.EXPORT) {
|
|
||||||
onModalToggle(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === MENU_KEY.CREATE_ALERTS) {
|
|
||||||
onCreateAlertsHandler();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onModalToggle, onCreateAlertsHandler],
|
|
||||||
);
|
|
||||||
|
|
||||||
const menu: MenuProps = useMemo(
|
|
||||||
() => ({
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
key: MENU_KEY.EXPORT,
|
|
||||||
label: MENU_LABEL.EXPORT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: MENU_KEY.CREATE_ALERTS,
|
|
||||||
label: MENU_LABEL.CREATE_ALERTS,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
onClick: onMenuClickHandler,
|
|
||||||
}),
|
|
||||||
[onMenuClickHandler],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onCancel = (value: boolean) => (): void => {
|
const onCancel = (value: boolean) => (): void => {
|
||||||
onModalToggle(value);
|
onModalToggle(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onAddToDashboard = (): void => {
|
||||||
|
setIsExport(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dropdown trigger={['click']} menu={menu}>
|
<Space size={24}>
|
||||||
<Button>Actions</Button>
|
<Button
|
||||||
</Dropdown>
|
icon={<AreaChartOutlined />}
|
||||||
|
onClick={onAddToDashboard}
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
Add to Dashboard
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={onCreateAlertsHandler} icon={<AlertOutlined />}>
|
||||||
|
Setup Alerts
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
footer={null}
|
footer={null}
|
||||||
onOk={onCancel(false)}
|
onOk={onCancel(false)}
|
||||||
onCancel={onCancel(false)}
|
onCancel={onCancel(false)}
|
||||||
open={isExport}
|
open={isExport}
|
||||||
centered
|
centered
|
||||||
|
destroyOnClose
|
||||||
>
|
>
|
||||||
<ExportPanelContainer
|
<ExportPanelContainer
|
||||||
query={query}
|
query={query}
|
||||||
@ -84,18 +70,12 @@ function ExportPanel({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ExportPanel.defaultProps = {
|
|
||||||
isLoading: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface OnClickProps {
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ExportPanelProps {
|
export interface ExportPanelProps {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
onExport: (dashboard: Dashboard | null) => void;
|
onExport: (dashboard: Dashboard | null) => void;
|
||||||
query: Query | null;
|
query: Query | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExportPanel.defaultProps = { isLoading: false };
|
||||||
|
|
||||||
export default ExportPanel;
|
export default ExportPanel;
|
||||||
|
@ -8,3 +8,11 @@ export const getSelectOptions = (
|
|||||||
label: data.title,
|
label: data.title,
|
||||||
value: uuid,
|
value: uuid,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const filterOptions: SelectProps['filterOption'] = (
|
||||||
|
input,
|
||||||
|
options,
|
||||||
|
): boolean =>
|
||||||
|
(options?.label?.toString() ?? '')
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes(input.toLowerCase());
|
||||||
|
@ -26,6 +26,7 @@ function FullView({
|
|||||||
name,
|
name,
|
||||||
yAxisUnit,
|
yAxisUnit,
|
||||||
onDragSelect,
|
onDragSelect,
|
||||||
|
isDependedDataLoaded = false,
|
||||||
}: FullViewProps): JSX.Element {
|
}: FullViewProps): JSX.Element {
|
||||||
const { selectedTime: globalSelectedTime } = useSelector<
|
const { selectedTime: globalSelectedTime } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
@ -61,6 +62,7 @@ function FullView({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
queryKey,
|
queryKey,
|
||||||
|
enabled: !isDependedDataLoaded,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -76,9 +78,7 @@ function FullView({
|
|||||||
[response],
|
[response],
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLoading = response.isLoading === true;
|
if (response.status === 'idle' || response.status === 'loading') {
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <Spinner height="100%" size="large" tip="Loading..." />;
|
return <Spinner height="100%" size="large" tip="Loading..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,6 +123,7 @@ interface FullViewProps {
|
|||||||
name: string;
|
name: string;
|
||||||
yAxisUnit?: string;
|
yAxisUnit?: string;
|
||||||
onDragSelect?: (start: number, end: number) => void;
|
onDragSelect?: (start: number, end: number) => void;
|
||||||
|
isDependedDataLoaded?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
FullView.defaultProps = {
|
FullView.defaultProps = {
|
||||||
@ -130,6 +131,7 @@ FullView.defaultProps = {
|
|||||||
onClickHandler: undefined,
|
onClickHandler: undefined,
|
||||||
yAxisUnit: undefined,
|
yAxisUnit: undefined,
|
||||||
onDragSelect: undefined,
|
onDragSelect: undefined,
|
||||||
|
isDependedDataLoaded: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FullView;
|
export default FullView;
|
||||||
|
@ -83,6 +83,11 @@ function GridCardGraph({
|
|||||||
|
|
||||||
const updatedQuery = useStepInterval(widget?.query);
|
const updatedQuery = useStepInterval(widget?.query);
|
||||||
|
|
||||||
|
const isEmptyWidget = useMemo(
|
||||||
|
() => widget?.id === 'empty' || isEmpty(widget),
|
||||||
|
[widget],
|
||||||
|
);
|
||||||
|
|
||||||
const queryResponse = useGetQueryRange(
|
const queryResponse = useGetQueryRange(
|
||||||
{
|
{
|
||||||
selectedTime: widget?.timePreferance,
|
selectedTime: widget?.timePreferance,
|
||||||
@ -101,7 +106,7 @@ function GridCardGraph({
|
|||||||
variables,
|
variables,
|
||||||
],
|
],
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
enabled: isGraphVisible,
|
enabled: isGraphVisible && !isEmptyWidget,
|
||||||
refetchOnMount: false,
|
refetchOnMount: false,
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
setErrorMessage(error.message);
|
setErrorMessage(error.message);
|
||||||
@ -131,8 +136,6 @@ function GridCardGraph({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onDeleteHandler = useCallback(() => {
|
const onDeleteHandler = useCallback(() => {
|
||||||
const isEmptyWidget = widget?.id === 'empty' || isEmpty(widget);
|
|
||||||
|
|
||||||
const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
|
const widgetId = isEmptyWidget ? layout[0].i : widget?.id;
|
||||||
|
|
||||||
featureResponse
|
featureResponse
|
||||||
@ -147,7 +150,8 @@ function GridCardGraph({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, [
|
}, [
|
||||||
widget,
|
isEmptyWidget,
|
||||||
|
widget?.id,
|
||||||
layout,
|
layout,
|
||||||
featureResponse,
|
featureResponse,
|
||||||
deleteWidget,
|
deleteWidget,
|
||||||
|
@ -9,7 +9,7 @@ import {
|
|||||||
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
import { Dropdown, MenuProps, Tooltip, Typography } from 'antd';
|
||||||
import { MenuItemType } from 'antd/es/menu/hooks/useItems';
|
import { MenuItemType } from 'antd/es/menu/hooks/useItems';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
|
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
@ -64,7 +64,9 @@ 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}=${encodeURIComponent(JSON.stringify(widget.query))}`,
|
}&${queryParamNamesMap.compositeQuery}=${encodeURIComponent(
|
||||||
|
JSON.stringify(widget.query),
|
||||||
|
)}`,
|
||||||
);
|
);
|
||||||
}, [widget.id, widget.panelTypes, widget.query]);
|
}, [widget.id, widget.panelTypes, widget.query]);
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { ColumnsType } from 'antd/lib/table';
|
|||||||
import saveAlertApi from 'api/alerts/save';
|
import saveAlertApi from 'api/alerts/save';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
|
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import useInterval from 'hooks/useInterval';
|
import useInterval from 'hooks/useInterval';
|
||||||
@ -75,11 +75,9 @@ function ListAlert({ allAlertRules, refetch }: ListAlertProps): JSX.Element {
|
|||||||
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
|
const compositeQuery = mapQueryDataFromApi(record.condition.compositeQuery);
|
||||||
|
|
||||||
history.push(
|
history.push(
|
||||||
`${
|
`${ROUTES.EDIT_ALERTS}?ruleId=${record.id.toString()}&${
|
||||||
ROUTES.EDIT_ALERTS
|
queryParamNamesMap.compositeQuery
|
||||||
}?ruleId=${record.id.toString()}&${COMPOSITE_QUERY}=${encodeURIComponent(
|
}=${encodeURIComponent(JSON.stringify(compositeQuery))}`,
|
||||||
JSON.stringify(compositeQuery),
|
|
||||||
)}`,
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch(handleError);
|
.catch(handleError);
|
||||||
|
@ -83,12 +83,17 @@ function LogControls(): JSX.Element | null {
|
|||||||
|
|
||||||
const flattenLogData = useMemo(
|
const flattenLogData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
logs.map((log) =>
|
logs.map((log) => {
|
||||||
FlatLogData({
|
const timestamp =
|
||||||
|
typeof log.timestamp === 'string'
|
||||||
|
? dayjs(log.timestamp).format()
|
||||||
|
: dayjs(log.timestamp / 1e6).format();
|
||||||
|
|
||||||
|
return FlatLogData({
|
||||||
...log,
|
...log,
|
||||||
timestamp: (dayjs(log.timestamp / 1e6).format() as unknown) as number,
|
timestamp,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
[logs],
|
[logs],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,148 +1,43 @@
|
|||||||
import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
|
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 { OPERATORS } from 'constants/queryBuilder';
|
||||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
|
||||||
import { getIdConditions } from 'pages/Logs/utils';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { memo, useMemo } from 'react';
|
|
||||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
|
||||||
import { getLogs } from 'store/actions/logs/getLogs';
|
|
||||||
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import AppActions from 'types/actions';
|
|
||||||
import { SET_SEARCH_QUERY_STRING, TOGGLE_LIVE_TAIL } from 'types/actions/logs';
|
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
|
||||||
import { ILogsReducer } from 'types/reducer/logs';
|
|
||||||
|
|
||||||
const removeJSONStringifyQuotes = (s: string): string => {
|
|
||||||
if (!s || !s.length) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s[0] === '"' && s[s.length - 1] === '"') {
|
|
||||||
return s.slice(1, s.length - 1);
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ActionItemProps {
|
|
||||||
fieldKey: string;
|
|
||||||
fieldValue: string;
|
|
||||||
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
|
||||||
getLogsAggregate: (
|
|
||||||
props: Parameters<typeof getLogsAggregate>[0],
|
|
||||||
) => ReturnType<typeof getLogsAggregate>;
|
|
||||||
}
|
|
||||||
function ActionItem({
|
function ActionItem({
|
||||||
fieldKey,
|
fieldKey,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
getLogs,
|
onClickActionItem,
|
||||||
getLogsAggregate,
|
}: ActionItemProps): JSX.Element {
|
||||||
}: ActionItemProps): JSX.Element | unknown {
|
const handleClick = useCallback(
|
||||||
const {
|
(operator: string) => {
|
||||||
searchFilter: { queryString },
|
|
||||||
logLinesPerPage,
|
|
||||||
idStart,
|
|
||||||
liveTail,
|
|
||||||
idEnd,
|
|
||||||
order,
|
|
||||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
|
||||||
|
|
||||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
|
||||||
(state) => state.globalTime,
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleQueryAdd = (newQueryString: string): void => {
|
|
||||||
let updatedQueryString = queryString || '';
|
|
||||||
|
|
||||||
if (updatedQueryString.length === 0) {
|
|
||||||
updatedQueryString += `${newQueryString}`;
|
|
||||||
} else {
|
|
||||||
updatedQueryString += ` AND ${newQueryString}`;
|
|
||||||
}
|
|
||||||
dispatch({
|
|
||||||
type: SET_SEARCH_QUERY_STRING,
|
|
||||||
payload: {
|
|
||||||
searchQueryString: updatedQueryString,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (liveTail === 'STOPPED') {
|
|
||||||
getLogs({
|
|
||||||
q: updatedQueryString,
|
|
||||||
limit: logLinesPerPage,
|
|
||||||
orderBy: 'timestamp',
|
|
||||||
order,
|
|
||||||
timestampStart: minTime,
|
|
||||||
timestampEnd: maxTime,
|
|
||||||
...getIdConditions(idStart, idEnd, order),
|
|
||||||
});
|
|
||||||
getLogsAggregate({
|
|
||||||
timestampStart: minTime,
|
|
||||||
timestampEnd: maxTime,
|
|
||||||
step: getStep({
|
|
||||||
start: minTime,
|
|
||||||
end: maxTime,
|
|
||||||
inputFormat: 'ns',
|
|
||||||
}),
|
|
||||||
q: updatedQueryString,
|
|
||||||
});
|
|
||||||
} else if (liveTail === 'PLAYING') {
|
|
||||||
dispatch({
|
|
||||||
type: TOGGLE_LIVE_TAIL,
|
|
||||||
payload: 'PAUSED',
|
|
||||||
});
|
|
||||||
setTimeout(
|
|
||||||
() =>
|
|
||||||
dispatch({
|
|
||||||
type: TOGGLE_LIVE_TAIL,
|
|
||||||
payload: liveTail,
|
|
||||||
}),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const validatedFieldValue = removeJSONStringifyQuotes(fieldValue);
|
const validatedFieldValue = removeJSONStringifyQuotes(fieldValue);
|
||||||
|
|
||||||
|
onClickActionItem(fieldKey, validatedFieldValue, operator);
|
||||||
|
},
|
||||||
|
[onClickActionItem, fieldKey, fieldValue],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClickHandler = useCallback(
|
||||||
|
(operator: string) => (): void => {
|
||||||
|
handleClick(operator);
|
||||||
|
},
|
||||||
|
[handleClick],
|
||||||
|
);
|
||||||
|
|
||||||
const PopOverMenuContent = useMemo(
|
const PopOverMenuContent = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<Col>
|
<Col>
|
||||||
<Button
|
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.IN)}>
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
onClick={(): void =>
|
|
||||||
handleQueryAdd(
|
|
||||||
generateFilterQuery({
|
|
||||||
fieldKey,
|
|
||||||
fieldValue: validatedFieldValue,
|
|
||||||
type: 'IN',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<PlusCircleOutlined /> Filter for value
|
<PlusCircleOutlined /> Filter for value
|
||||||
</Button>
|
</Button>
|
||||||
<br />
|
<br />
|
||||||
<Button
|
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.NIN)}>
|
||||||
type="text"
|
|
||||||
size="small"
|
|
||||||
onClick={(): void =>
|
|
||||||
handleQueryAdd(
|
|
||||||
generateFilterQuery({
|
|
||||||
fieldKey,
|
|
||||||
fieldValue: validatedFieldValue,
|
|
||||||
type: 'NIN',
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MinusCircleOutlined /> Filter out value
|
<MinusCircleOutlined /> Filter out value
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
),
|
),
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
[onClickHandler],
|
||||||
[fieldKey, validatedFieldValue],
|
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<Popover placement="bottomLeft" content={PopOverMenuContent} trigger="click">
|
<Popover placement="bottomLeft" content={PopOverMenuContent} trigger="click">
|
||||||
@ -152,19 +47,15 @@ function ActionItem({
|
|||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
interface DispatchProps {
|
|
||||||
getLogs: (props: Parameters<typeof getLogs>[0]) => (dispatch: never) => void;
|
export interface ActionItemProps {
|
||||||
getLogsAggregate: (
|
fieldKey: string;
|
||||||
props: Parameters<typeof getLogsAggregate>[0],
|
fieldValue: string;
|
||||||
) => (dispatch: never) => void;
|
onClickActionItem: (
|
||||||
|
fieldKey: string,
|
||||||
|
fieldValue: string,
|
||||||
|
operator: string,
|
||||||
|
) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = (
|
export default memo(ActionItem);
|
||||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
|
||||||
): DispatchProps => ({
|
|
||||||
getLogs: bindActionCreators(getLogs, dispatch),
|
|
||||||
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export default connect(null, mapDispatchToProps)(memo(ActionItem as any));
|
|
||||||
|
@ -3,7 +3,9 @@ import { LinkOutlined } from '@ant-design/icons';
|
|||||||
import { Input, Space, Tooltip } from 'antd';
|
import { Input, Space, Tooltip } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import Editor from 'components/Editor';
|
import Editor from 'components/Editor';
|
||||||
import AddToQueryHOC from 'components/Logs/AddToQueryHOC';
|
import AddToQueryHOC, {
|
||||||
|
AddToQueryHOCProps,
|
||||||
|
} from 'components/Logs/AddToQueryHOC';
|
||||||
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
@ -18,7 +20,7 @@ import AppActions from 'types/actions';
|
|||||||
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
import ActionItem from './ActionItem';
|
import ActionItem, { ActionItemProps } from './ActionItem';
|
||||||
import { flattenObject, recursiveParseJSON } from './utils';
|
import { flattenObject, recursiveParseJSON } from './utils';
|
||||||
|
|
||||||
// Fields which should be restricted from adding it to query
|
// Fields which should be restricted from adding it to query
|
||||||
@ -27,7 +29,16 @@ const RESTRICTED_FIELDS = ['timestamp'];
|
|||||||
interface TableViewProps {
|
interface TableViewProps {
|
||||||
logData: ILog;
|
logData: ILog;
|
||||||
}
|
}
|
||||||
function TableView({ logData }: TableViewProps): JSX.Element | null {
|
|
||||||
|
type Props = TableViewProps &
|
||||||
|
Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||||
|
Pick<ActionItemProps, 'onClickActionItem'>;
|
||||||
|
|
||||||
|
function TableView({
|
||||||
|
logData,
|
||||||
|
onAddToQuery,
|
||||||
|
onClickActionItem,
|
||||||
|
}: Props): JSX.Element | null {
|
||||||
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
|
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
|
||||||
|
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
@ -84,7 +95,13 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
|
|||||||
render: (fieldData: Record<string, string>): JSX.Element | null => {
|
render: (fieldData: Record<string, string>): JSX.Element | null => {
|
||||||
const fieldKey = fieldData.field.split('.').slice(-1);
|
const fieldKey = fieldData.field.split('.').slice(-1);
|
||||||
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
|
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
|
||||||
return <ActionItem fieldKey={fieldKey} fieldValue={fieldData.value} />;
|
return (
|
||||||
|
<ActionItem
|
||||||
|
fieldKey={fieldKey[0]}
|
||||||
|
fieldValue={fieldData.value}
|
||||||
|
onClickActionItem={onClickActionItem}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
@ -128,7 +145,11 @@ function TableView({ logData }: TableViewProps): JSX.Element | null {
|
|||||||
|
|
||||||
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
|
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
|
||||||
return (
|
return (
|
||||||
<AddToQueryHOC fieldKey={fieldKey[0]} fieldValue={flattenLogData[field]}>
|
<AddToQueryHOC
|
||||||
|
fieldKey={fieldKey[0]}
|
||||||
|
fieldValue={flattenLogData[field]}
|
||||||
|
onAddToQuery={onAddToQuery}
|
||||||
|
>
|
||||||
{renderedField}
|
{renderedField}
|
||||||
</AddToQueryHOC>
|
</AddToQueryHOC>
|
||||||
);
|
);
|
||||||
|
@ -1,17 +1,48 @@
|
|||||||
import { Drawer, Tabs } from 'antd';
|
import LogDetail from 'components/LogDetail';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import ROUTES from 'constants/routes';
|
||||||
import { Dispatch } from 'redux';
|
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
|
||||||
|
import getStep from 'lib/getStep';
|
||||||
|
import { getIdConditions } from 'pages/Logs/utils';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { getLogs } from 'store/actions/logs/getLogs';
|
||||||
|
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
import {
|
||||||
|
SET_DETAILED_LOG_DATA,
|
||||||
|
SET_SEARCH_QUERY_STRING,
|
||||||
|
TOGGLE_LIVE_TAIL,
|
||||||
|
} from 'types/actions/logs';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
import { ILogsReducer } from 'types/reducer/logs';
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
import JSONView from './JsonView';
|
type LogDetailedViewProps = {
|
||||||
import TableView from './TableView';
|
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||||
|
getLogsAggregate: (
|
||||||
|
props: Parameters<typeof getLogsAggregate>[0],
|
||||||
|
) => ReturnType<typeof getLogsAggregate>;
|
||||||
|
};
|
||||||
|
|
||||||
function LogDetailedView(): JSX.Element {
|
function LogDetailedView({
|
||||||
const { detailedLog } = useSelector<AppState, ILogsReducer>(
|
getLogs,
|
||||||
(state) => state.logs,
|
getLogsAggregate,
|
||||||
|
}: LogDetailedViewProps): JSX.Element {
|
||||||
|
const history = useHistory();
|
||||||
|
const {
|
||||||
|
detailedLog,
|
||||||
|
searchFilter: { queryString },
|
||||||
|
logLinesPerPage,
|
||||||
|
idStart,
|
||||||
|
liveTail,
|
||||||
|
idEnd,
|
||||||
|
order,
|
||||||
|
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
);
|
);
|
||||||
|
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||||
@ -23,33 +54,109 @@ function LogDetailedView(): JSX.Element {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const items = [
|
const handleAddToQuery = useCallback(
|
||||||
{
|
(fieldKey: string, fieldValue: string, operator: string) => {
|
||||||
label: 'Table',
|
const updatedQueryString = getGeneratedFilterQueryString(
|
||||||
key: '1',
|
fieldKey,
|
||||||
children: detailedLog && <TableView logData={detailedLog} />,
|
fieldValue,
|
||||||
|
operator,
|
||||||
|
queryString,
|
||||||
|
);
|
||||||
|
|
||||||
|
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
|
||||||
},
|
},
|
||||||
{
|
[history, queryString],
|
||||||
label: 'JSON',
|
);
|
||||||
key: '2',
|
|
||||||
children: detailedLog && <JSONView logData={detailedLog} />,
|
const handleClickActionItem = useCallback(
|
||||||
|
(fieldKey: string, fieldValue: string, operator: string): void => {
|
||||||
|
const updatedQueryString = getGeneratedFilterQueryString(
|
||||||
|
fieldKey,
|
||||||
|
fieldValue,
|
||||||
|
operator,
|
||||||
|
queryString,
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: SET_SEARCH_QUERY_STRING,
|
||||||
|
payload: {
|
||||||
|
searchQueryString: updatedQueryString,
|
||||||
},
|
},
|
||||||
];
|
});
|
||||||
|
|
||||||
|
if (liveTail === 'STOPPED') {
|
||||||
|
getLogs({
|
||||||
|
q: updatedQueryString,
|
||||||
|
limit: logLinesPerPage,
|
||||||
|
orderBy: 'timestamp',
|
||||||
|
order,
|
||||||
|
timestampStart: minTime,
|
||||||
|
timestampEnd: maxTime,
|
||||||
|
...getIdConditions(idStart, idEnd, order),
|
||||||
|
});
|
||||||
|
getLogsAggregate({
|
||||||
|
timestampStart: minTime,
|
||||||
|
timestampEnd: maxTime,
|
||||||
|
step: getStep({
|
||||||
|
start: minTime,
|
||||||
|
end: maxTime,
|
||||||
|
inputFormat: 'ns',
|
||||||
|
}),
|
||||||
|
q: updatedQueryString,
|
||||||
|
});
|
||||||
|
} else if (liveTail === 'PLAYING') {
|
||||||
|
dispatch({
|
||||||
|
type: TOGGLE_LIVE_TAIL,
|
||||||
|
payload: 'PAUSED',
|
||||||
|
});
|
||||||
|
setTimeout(
|
||||||
|
() =>
|
||||||
|
dispatch({
|
||||||
|
type: TOGGLE_LIVE_TAIL,
|
||||||
|
payload: liveTail,
|
||||||
|
}),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
dispatch,
|
||||||
|
getLogs,
|
||||||
|
getLogsAggregate,
|
||||||
|
idEnd,
|
||||||
|
idStart,
|
||||||
|
liveTail,
|
||||||
|
logLinesPerPage,
|
||||||
|
maxTime,
|
||||||
|
minTime,
|
||||||
|
order,
|
||||||
|
queryString,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<LogDetail
|
||||||
width="60%"
|
log={detailedLog}
|
||||||
title="Log Details"
|
|
||||||
placement="right"
|
|
||||||
closable
|
|
||||||
onClose={onDrawerClose}
|
onClose={onDrawerClose}
|
||||||
open={detailedLog !== null}
|
onAddToQuery={handleAddToQuery}
|
||||||
style={{ overscrollBehavior: 'contain' }}
|
onClickActionItem={handleClickActionItem}
|
||||||
destroyOnClose
|
/>
|
||||||
>
|
|
||||||
<Tabs defaultActiveKey="1" items={items} />
|
|
||||||
</Drawer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LogDetailedView;
|
interface DispatchProps {
|
||||||
|
getLogs: (props: Parameters<typeof getLogs>[0]) => (dispatch: never) => void;
|
||||||
|
getLogsAggregate: (
|
||||||
|
props: Parameters<typeof getLogsAggregate>[0],
|
||||||
|
) => (dispatch: never) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||||
|
): DispatchProps => ({
|
||||||
|
getLogs: bindActionCreators(getLogs, dispatch),
|
||||||
|
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export default connect(null, mapDispatchToProps)(memo(LogDetailedView as any));
|
||||||
|
51
frontend/src/container/LogExplorerQuerySection/index.tsx
Normal file
51
frontend/src/container/LogExplorerQuerySection/index.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Button } from 'antd';
|
||||||
|
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
|
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||||
|
import { ButtonWrapperStyled } from 'pages/LogsExplorer/styles';
|
||||||
|
import { prepareQueryWithDefaultTimestamp } from 'pages/LogsExplorer/utils';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
function LogExplorerQuerySection(): JSX.Element {
|
||||||
|
const { handleRunQuery, updateAllQueriesOperators } = useQueryBuilder();
|
||||||
|
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||||
|
const defaultValue = useMemo(() => {
|
||||||
|
const updatedQuery = updateAllQueriesOperators(
|
||||||
|
initialQueriesMap.logs,
|
||||||
|
PANEL_TYPES.LIST,
|
||||||
|
DataSource.LOGS,
|
||||||
|
);
|
||||||
|
return prepareQueryWithDefaultTimestamp(updatedQuery);
|
||||||
|
}, [updateAllQueriesOperators]);
|
||||||
|
|
||||||
|
useShareBuilderUrl(defaultValue);
|
||||||
|
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||||
|
const isTable = panelTypes === PANEL_TYPES.TABLE;
|
||||||
|
const config: QueryBuilderProps['filterConfigs'] = {
|
||||||
|
stepInterval: { isHidden: isTable, isDisabled: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}, [panelTypes]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryBuilder
|
||||||
|
panelType={panelTypes}
|
||||||
|
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
|
||||||
|
filterConfigs={filterConfigs}
|
||||||
|
actions={
|
||||||
|
<ButtonWrapperStyled>
|
||||||
|
<Button type="primary" onClick={handleRunQuery}>
|
||||||
|
Run Query
|
||||||
|
</Button>
|
||||||
|
</ButtonWrapperStyled>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(LogExplorerQuerySection);
|
@ -0,0 +1,6 @@
|
|||||||
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
|
|
||||||
|
export type LogsExplorerChartProps = {
|
||||||
|
data: QueryData[];
|
||||||
|
isLoading: boolean;
|
||||||
|
};
|
@ -1,48 +1,43 @@
|
|||||||
import Graph from 'components/Graph';
|
import Graph from 'components/Graph';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import getChartData, { GetChartDataProps } from 'lib/getChartData';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { colors } from 'lib/getRandomColor';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|
||||||
import { getExplorerChartData } from 'lib/explorer/getExplorerChartData';
|
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { AppState } from 'store/reducers';
|
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
|
||||||
|
|
||||||
|
import { LogsExplorerChartProps } from './LogsExplorerChart.interfaces';
|
||||||
import { CardStyled } from './LogsExplorerChart.styled';
|
import { CardStyled } from './LogsExplorerChart.styled';
|
||||||
|
|
||||||
function LogsExplorerChart(): JSX.Element {
|
function LogsExplorerChart({
|
||||||
const { stagedQuery, panelType, isEnabledQuery } = useQueryBuilder();
|
data,
|
||||||
|
isLoading,
|
||||||
|
}: LogsExplorerChartProps): JSX.Element {
|
||||||
|
const handleCreateDatasets: Required<GetChartDataProps>['createDataset'] = (
|
||||||
|
element,
|
||||||
|
index,
|
||||||
|
allLabels,
|
||||||
|
) => ({
|
||||||
|
label: allLabels[index],
|
||||||
|
data: element,
|
||||||
|
backgroundColor: colors[index % colors.length] || 'red',
|
||||||
|
borderColor: colors[index % colors.length] || 'red',
|
||||||
|
});
|
||||||
|
|
||||||
const { selectedTime } = useSelector<AppState, GlobalReducer>(
|
const graphData = useMemo(
|
||||||
(state) => state.globalTime,
|
() =>
|
||||||
);
|
getChartData({
|
||||||
|
queryData: [
|
||||||
const { data, isFetching } = useGetQueryRange(
|
|
||||||
{
|
{
|
||||||
query: stagedQuery || initialQueriesMap.metrics,
|
queryData: data,
|
||||||
graphType: panelType || PANEL_TYPES.LIST,
|
|
||||||
globalSelectedInterval: selectedTime,
|
|
||||||
selectedTime: 'GLOBAL_TIME',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
queryKey: [REACT_QUERY_KEY.GET_QUERY_RANGE, selectedTime, stagedQuery],
|
|
||||||
enabled: isEnabledQuery,
|
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
createDataset: handleCreateDatasets,
|
||||||
|
}),
|
||||||
|
[data],
|
||||||
);
|
);
|
||||||
|
|
||||||
const graphData = useMemo(() => {
|
|
||||||
if (data?.payload.data && data.payload.data.result.length > 0) {
|
|
||||||
return getExplorerChartData([data.payload.data.result[0]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return getExplorerChartData([]);
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CardStyled>
|
<CardStyled>
|
||||||
{isFetching ? (
|
{isLoading ? (
|
||||||
<Spinner size="default" height="100%" />
|
<Spinner size="default" height="100%" />
|
||||||
) : (
|
) : (
|
||||||
<Graph
|
<Graph
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
export const infinityDefaultStyles: CSSProperties = {
|
||||||
|
height: 'auto',
|
||||||
|
width: '100%',
|
||||||
|
};
|
@ -0,0 +1,98 @@
|
|||||||
|
import { ColumnTypeRender } from 'components/Logs/TableView/types';
|
||||||
|
import { useTableView } from 'components/Logs/TableView/useTableView';
|
||||||
|
import { cloneElement, ReactElement, ReactNode, useCallback } from 'react';
|
||||||
|
import { TableComponents, TableVirtuoso } from 'react-virtuoso';
|
||||||
|
|
||||||
|
import { infinityDefaultStyles } from './config';
|
||||||
|
import {
|
||||||
|
TableCellStyled,
|
||||||
|
TableHeaderCellStyled,
|
||||||
|
TableRowStyled,
|
||||||
|
TableStyled,
|
||||||
|
} from './styles';
|
||||||
|
import { InfinityTableProps } from './types';
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/function-component-definition
|
||||||
|
const CustomTable: TableComponents['Table'] = ({ style, children }) => (
|
||||||
|
<TableStyled style={style}>{children}</TableStyled>
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/function-component-definition
|
||||||
|
const CustomTableRow: TableComponents['TableRow'] = ({
|
||||||
|
children,
|
||||||
|
context,
|
||||||
|
...props
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
}) => <TableRowStyled {...props}>{children}</TableRowStyled>;
|
||||||
|
|
||||||
|
function InfinityTable({
|
||||||
|
tableViewProps,
|
||||||
|
infitiyTableProps,
|
||||||
|
}: InfinityTableProps): JSX.Element | null {
|
||||||
|
const { onEndReached } = infitiyTableProps;
|
||||||
|
const { dataSource, columns } = useTableView(tableViewProps);
|
||||||
|
|
||||||
|
const itemContent = useCallback(
|
||||||
|
(index: number, log: Record<string, unknown>): JSX.Element => (
|
||||||
|
<>
|
||||||
|
{columns.map((column) => {
|
||||||
|
if (!column.render) return <td>Empty</td>;
|
||||||
|
|
||||||
|
const element: ColumnTypeRender<Record<string, unknown>> = column.render(
|
||||||
|
log[column.key as keyof Record<string, unknown>],
|
||||||
|
log,
|
||||||
|
index,
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementWithChildren = element as Exclude<
|
||||||
|
ColumnTypeRender<Record<string, unknown>>,
|
||||||
|
ReactNode
|
||||||
|
>;
|
||||||
|
|
||||||
|
const children = elementWithChildren.children as ReactElement;
|
||||||
|
const props = elementWithChildren.props as Record<string, unknown>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableCellStyled key={column.key}>
|
||||||
|
{cloneElement(children, props)}
|
||||||
|
</TableCellStyled>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
[columns],
|
||||||
|
);
|
||||||
|
|
||||||
|
const tableHeader = useCallback(
|
||||||
|
() => (
|
||||||
|
<tr>
|
||||||
|
{columns.map((column) => (
|
||||||
|
<TableHeaderCellStyled key={column.key}>
|
||||||
|
{column.title as string}
|
||||||
|
</TableHeaderCellStyled>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
),
|
||||||
|
[columns],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableVirtuoso
|
||||||
|
style={infinityDefaultStyles}
|
||||||
|
data={dataSource}
|
||||||
|
components={{
|
||||||
|
Table: CustomTable,
|
||||||
|
// TODO: fix it in the future
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
TableRow: CustomTableRow,
|
||||||
|
}}
|
||||||
|
itemContent={itemContent}
|
||||||
|
fixedHeaderContent={tableHeader}
|
||||||
|
endReached={onEndReached}
|
||||||
|
totalCount={dataSource.length}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InfinityTable;
|
@ -0,0 +1,40 @@
|
|||||||
|
import { themeColors } from 'constants/theme';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const TableStyled = styled.table`
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid rgba(253, 253, 253, 0.12);
|
||||||
|
border-radius: 2px 2px 0 0;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
border-inline-start: 1px solid rgba(253, 253, 253, 0.12);
|
||||||
|
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TableCellStyled = styled.td`
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
|
||||||
|
border-top: 1px solid rgba(253, 253, 253, 0.12);
|
||||||
|
background-color: ${themeColors.lightBlack};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TableRowStyled = styled.tr`
|
||||||
|
&:hover {
|
||||||
|
${TableCellStyled} {
|
||||||
|
background-color: #1d1d1d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const TableHeaderCellStyled = styled.th`
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-inline-end: 1px solid rgba(253, 253, 253, 0.12);
|
||||||
|
background-color: #1d1d1d;
|
||||||
|
&:first-child {
|
||||||
|
border-start-start-radius: 2px;
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
border-start-end-radius: 2px;
|
||||||
|
border-inline-end: none;
|
||||||
|
}
|
||||||
|
`;
|
@ -0,0 +1,8 @@
|
|||||||
|
import { UseTableViewProps } from 'components/Logs/TableView/types';
|
||||||
|
|
||||||
|
export type InfinityTableProps = {
|
||||||
|
tableViewProps: UseTableViewProps;
|
||||||
|
infitiyTableProps: {
|
||||||
|
onEndReached: (index: number) => void;
|
||||||
|
};
|
||||||
|
};
|
@ -1,3 +1,12 @@
|
|||||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
export type LogsExplorerListProps = { data: QueryDataV3[]; isLoading: boolean };
|
export type LogsExplorerListProps = {
|
||||||
|
isLoading: boolean;
|
||||||
|
currentStagedQueryData: IBuilderQuery | null;
|
||||||
|
logs: ILog[];
|
||||||
|
onEndReached: (index: number) => void;
|
||||||
|
onExpand: (log: ILog) => void;
|
||||||
|
onOpenDetailedView: (log: ILog) => void;
|
||||||
|
} & Pick<AddToQueryHOCProps, 'onAddToQuery'>;
|
||||||
|
@ -2,38 +2,46 @@ import { Card, Typography } from 'antd';
|
|||||||
// components
|
// components
|
||||||
import ListLogView from 'components/Logs/ListLogView';
|
import ListLogView from 'components/Logs/ListLogView';
|
||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
import LogsTableView from 'components/Logs/TableView';
|
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
import { LogViewMode } from 'container/LogsTable';
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { Container, Heading } from 'container/LogsTable/styles';
|
import ExplorerControlPanel from 'container/ExplorerControlPanel';
|
||||||
|
import { Heading } from 'container/LogsTable/styles';
|
||||||
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
import { contentStyle } from 'container/Trace/Search/config';
|
import { contentStyle } from 'container/Trace/Search/config';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import useFontFaceObserver from 'hooks/useFontObserver';
|
import useFontFaceObserver from 'hooks/useFontObserver';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
// interfaces
|
// interfaces
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import InfinityTableView from './InfinityTableView';
|
||||||
import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
|
import { LogsExplorerListProps } from './LogsExplorerList.interfaces';
|
||||||
|
import { InfinityWrapperStyled } from './styles';
|
||||||
|
import { convertKeysToColumnFields } from './utils';
|
||||||
|
|
||||||
|
function Footer(): JSX.Element {
|
||||||
|
return <Spinner height={20} tip="Getting Logs" />;
|
||||||
|
}
|
||||||
|
|
||||||
function LogsExplorerList({
|
function LogsExplorerList({
|
||||||
data,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
|
currentStagedQueryData,
|
||||||
|
logs,
|
||||||
|
onOpenDetailedView,
|
||||||
|
onEndReached,
|
||||||
|
onExpand,
|
||||||
|
onAddToQuery,
|
||||||
}: LogsExplorerListProps): JSX.Element {
|
}: LogsExplorerListProps): JSX.Element {
|
||||||
const [viewMode] = useState<LogViewMode>('raw');
|
const { initialDataSource } = useQueryBuilder();
|
||||||
const [linesPerRow] = useState<number>(20);
|
|
||||||
|
|
||||||
const logs: ILog[] = useMemo(() => {
|
const { options, config } = useOptionsMenu({
|
||||||
if (data.length > 0 && data[0].list) {
|
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
|
||||||
const logs: ILog[] = data[0].list.map((item) => ({
|
dataSource: initialDataSource || DataSource.METRICS,
|
||||||
timestamp: +item.timestamp,
|
aggregateOperator:
|
||||||
...item.data,
|
currentStagedQueryData?.aggregateOperator || StringOperators.NOOP,
|
||||||
}));
|
});
|
||||||
|
|
||||||
return logs;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
useFontFaceObserver(
|
useFontFaceObserver(
|
||||||
[
|
[
|
||||||
@ -42,75 +50,110 @@ function LogsExplorerList({
|
|||||||
weight: '300',
|
weight: '300',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
viewMode === 'raw',
|
options.format === 'raw',
|
||||||
{
|
{
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// TODO: implement here linesPerRow, mode like in useSelectedLogView
|
|
||||||
|
const selectedFields = useMemo(
|
||||||
|
() => convertKeysToColumnFields(options.selectColumns),
|
||||||
|
[options],
|
||||||
|
);
|
||||||
|
|
||||||
const getItemContent = useCallback(
|
const getItemContent = useCallback(
|
||||||
(index: number): JSX.Element => {
|
(_: number, log: ILog): JSX.Element => {
|
||||||
const log = logs[index];
|
if (options.format === 'raw') {
|
||||||
|
|
||||||
if (viewMode === 'raw') {
|
|
||||||
return (
|
return (
|
||||||
<RawLogView
|
<RawLogView
|
||||||
key={log.id}
|
key={log.id}
|
||||||
data={log}
|
data={log}
|
||||||
linesPerRow={linesPerRow}
|
linesPerRow={options.maxLines}
|
||||||
// TODO: write new onClickExpanded logic
|
onClickExpand={onExpand}
|
||||||
onClickExpand={(): void => {}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ListLogView key={log.id} logData={log} />;
|
return (
|
||||||
|
<ListLogView
|
||||||
|
key={log.id}
|
||||||
|
logData={log}
|
||||||
|
selectedFields={selectedFields}
|
||||||
|
onOpenDetailedView={onOpenDetailedView}
|
||||||
|
onAddToQuery={onAddToQuery}
|
||||||
|
/>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[logs, linesPerRow, viewMode],
|
[
|
||||||
|
options.format,
|
||||||
|
options.maxLines,
|
||||||
|
selectedFields,
|
||||||
|
onOpenDetailedView,
|
||||||
|
onAddToQuery,
|
||||||
|
onExpand,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderContent = useMemo(() => {
|
const renderContent = useMemo(() => {
|
||||||
if (viewMode === 'table') {
|
const components = isLoading
|
||||||
|
? {
|
||||||
|
Footer,
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
if (options.format === 'table') {
|
||||||
return (
|
return (
|
||||||
<LogsTableView
|
<InfinityTableView
|
||||||
logs={logs}
|
tableViewProps={{
|
||||||
// TODO: write new selected logic
|
logs,
|
||||||
fields={[]}
|
fields: selectedFields,
|
||||||
linesPerRow={linesPerRow}
|
linesPerRow: options.maxLines,
|
||||||
// TODO: write new onClickExpanded logic
|
onClickExpand: onExpand,
|
||||||
onClickExpand={(): void => {}}
|
appendTo: 'end',
|
||||||
|
}}
|
||||||
|
infitiyTableProps={{ onEndReached }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card bodyStyle={contentStyle}>
|
<Card style={{ width: '100%' }} bodyStyle={{ ...contentStyle }}>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
useWindowScroll
|
useWindowScroll
|
||||||
|
data={logs}
|
||||||
|
endReached={onEndReached}
|
||||||
totalCount={logs.length}
|
totalCount={logs.length}
|
||||||
itemContent={getItemContent}
|
itemContent={getItemContent}
|
||||||
|
components={components}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}, [getItemContent, linesPerRow, logs, viewMode]);
|
}, [
|
||||||
|
isLoading,
|
||||||
if (isLoading) {
|
logs,
|
||||||
return <Spinner height={20} tip="Getting Logs" />;
|
options.format,
|
||||||
}
|
options.maxLines,
|
||||||
|
onEndReached,
|
||||||
|
getItemContent,
|
||||||
|
selectedFields,
|
||||||
|
onExpand,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<>
|
||||||
{viewMode !== 'table' && (
|
<ExplorerControlPanel
|
||||||
|
isLoading={isLoading}
|
||||||
|
isShowPageSize={false}
|
||||||
|
optionsMenuConfig={config}
|
||||||
|
/>
|
||||||
|
{options.format !== 'table' && (
|
||||||
<Heading>
|
<Heading>
|
||||||
<Typography.Text>Event</Typography.Text>
|
<Typography.Text>Event</Typography.Text>
|
||||||
</Heading>
|
</Heading>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{logs.length === 0 && <Typography>No logs lines found</Typography>}
|
{logs.length === 0 && <Typography>No logs lines found</Typography>}
|
||||||
|
<InfinityWrapperStyled>{renderContent}</InfinityWrapperStyled>
|
||||||
{renderContent}
|
</>
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
frontend/src/container/LogsExplorerList/styles.ts
Normal file
6
frontend/src/container/LogsExplorerList/styles.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
export const InfinityWrapperStyled = styled.div`
|
||||||
|
min-height: 40rem;
|
||||||
|
display: flex;
|
||||||
|
`;
|
11
frontend/src/container/LogsExplorerList/utils.ts
Normal file
11
frontend/src/container/LogsExplorerList/utils.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { IField } from 'types/api/logs/fields';
|
||||||
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
|
||||||
|
export const convertKeysToColumnFields = (
|
||||||
|
keys: BaseAutocompleteData[],
|
||||||
|
): IField[] =>
|
||||||
|
keys.map((item) => ({
|
||||||
|
dataType: item.dataType as string,
|
||||||
|
name: item.key,
|
||||||
|
type: item.type as string,
|
||||||
|
}));
|
@ -6,8 +6,8 @@ import { memo } from 'react';
|
|||||||
import { LogsExplorerTableProps } from './LogsExplorerTable.interfaces';
|
import { LogsExplorerTableProps } from './LogsExplorerTable.interfaces';
|
||||||
|
|
||||||
function LogsExplorerTable({
|
function LogsExplorerTable({
|
||||||
isLoading,
|
|
||||||
data,
|
data,
|
||||||
|
isLoading,
|
||||||
}: LogsExplorerTableProps): JSX.Element {
|
}: LogsExplorerTableProps): JSX.Element {
|
||||||
const { stagedQuery } = useQueryBuilder();
|
const { stagedQuery } = useQueryBuilder();
|
||||||
|
|
||||||
|
@ -7,3 +7,9 @@ export const TabsStyled = styled(Tabs)`
|
|||||||
background-color: ${themeColors.lightBlack};
|
background-color: ${themeColors.lightBlack};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const ActionsWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
`;
|
||||||
|
@ -1,50 +1,100 @@
|
|||||||
import { TabsProps } from 'antd';
|
import { TabsProps } from 'antd';
|
||||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
import axios from 'axios';
|
||||||
import { PANEL_TYPES_QUERY } from 'constants/queryBuilderQueryNames';
|
import LogDetail from 'components/LogDetail';
|
||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import TabLabel from 'components/TabLabel';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
|
import {
|
||||||
|
initialQueriesMap,
|
||||||
|
OPERATORS,
|
||||||
|
PANEL_TYPES,
|
||||||
|
QueryBuilderKeys,
|
||||||
|
} from 'constants/queryBuilder';
|
||||||
|
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
||||||
|
import ExportPanel from 'container/ExportPanel';
|
||||||
|
import LogsExplorerChart from 'container/LogsExplorerChart';
|
||||||
import LogsExplorerList from 'container/LogsExplorerList';
|
import LogsExplorerList from 'container/LogsExplorerList';
|
||||||
import LogsExplorerTable from 'container/LogsExplorerTable';
|
// TODO: temporary hide table view
|
||||||
|
// import LogsExplorerTable from 'container/LogsExplorerTable';
|
||||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||||
|
import { addEmptyWidgetInDashboardJSONWithQuery } from 'hooks/dashboard/utils';
|
||||||
|
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
|
||||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
import { memo, useCallback, useEffect, useMemo } from 'react';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
|
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||||
|
import { getPaginationQueryData } from 'lib/newQueryBuilder/getPaginationQueryData';
|
||||||
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useQueryClient } from 'react-query';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import { generatePath, useHistory } from 'react-router-dom';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { SuccessResponse } from 'types/api';
|
||||||
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
import {
|
||||||
|
BaseAutocompleteData,
|
||||||
|
IQueryAutocompleteResponse,
|
||||||
|
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
import {
|
||||||
|
IBuilderQuery,
|
||||||
|
OrderByPayload,
|
||||||
|
Query,
|
||||||
|
} from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { TabsStyled } from './LogsExplorerViews.styled';
|
import { ActionsWrapper, TabsStyled } from './LogsExplorerViews.styled';
|
||||||
|
|
||||||
function LogsExplorerViews(): JSX.Element {
|
function LogsExplorerViews(): JSX.Element {
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const { queryData: pageSize } = useUrlQueryData(
|
||||||
|
queryParamNamesMap.pageSize,
|
||||||
|
DEFAULT_PER_PAGE_VALUE,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentMinTimeRef = useRef<number>(minTime);
|
||||||
|
|
||||||
|
// Context
|
||||||
const {
|
const {
|
||||||
currentQuery,
|
currentQuery,
|
||||||
stagedQuery,
|
stagedQuery,
|
||||||
panelType,
|
panelType,
|
||||||
isEnabledQuery,
|
|
||||||
updateAllQueriesOperators,
|
updateAllQueriesOperators,
|
||||||
redirectWithQueryBuilderData,
|
redirectWithQueryBuilderData,
|
||||||
} = useQueryBuilder();
|
} = useQueryBuilder();
|
||||||
|
|
||||||
const { selectedTime } = useSelector<AppState, GlobalReducer>(
|
// State
|
||||||
(state) => state.globalTime,
|
const [activeLog, setActiveLog] = useState<ILog | null>(null);
|
||||||
|
const [page, setPage] = useState<number>(1);
|
||||||
|
const [logs, setLogs] = useState<ILog[]>([]);
|
||||||
|
const [requestData, setRequestData] = useState<Query | null>(null);
|
||||||
|
|
||||||
|
const currentStagedQueryData = useMemo(() => {
|
||||||
|
if (!stagedQuery || stagedQuery.builder.queryData.length !== 1) return null;
|
||||||
|
|
||||||
|
return stagedQuery.builder.queryData[0];
|
||||||
|
}, [stagedQuery]);
|
||||||
|
|
||||||
|
const orderByTimestamp: OrderByPayload | null = useMemo(() => {
|
||||||
|
const timestampOrderBy = currentStagedQueryData?.orderBy.find(
|
||||||
|
(item) => item.columnName === 'timestamp',
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, isFetching, isError } = useGetQueryRange(
|
return timestampOrderBy || null;
|
||||||
{
|
}, [currentStagedQueryData]);
|
||||||
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(
|
const isMultipleQueries = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -62,35 +112,67 @@ function LogsExplorerViews(): JSX.Element {
|
|||||||
return groupByCount > 0;
|
return groupByCount > 0;
|
||||||
}, [currentQuery]);
|
}, [currentQuery]);
|
||||||
|
|
||||||
const currentData = useMemo(
|
const isLimit: boolean = useMemo(() => {
|
||||||
() => data?.payload.data.newResult.data.result || [],
|
if (!currentStagedQueryData) return false;
|
||||||
[data],
|
if (!currentStagedQueryData.limit) return false;
|
||||||
|
|
||||||
|
return logs.length >= currentStagedQueryData.limit;
|
||||||
|
}, [logs.length, currentStagedQueryData]);
|
||||||
|
|
||||||
|
const listChartQuery = useMemo(() => {
|
||||||
|
if (!stagedQuery || !currentStagedQueryData) return null;
|
||||||
|
|
||||||
|
const modifiedQueryData: IBuilderQuery = {
|
||||||
|
...currentStagedQueryData,
|
||||||
|
aggregateOperator: StringOperators.COUNT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const modifiedQuery: Query = {
|
||||||
|
...stagedQuery,
|
||||||
|
builder: {
|
||||||
|
...stagedQuery.builder,
|
||||||
|
queryData: stagedQuery.builder.queryData.map((item) => ({
|
||||||
|
...item,
|
||||||
|
...modifiedQueryData,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return modifiedQuery;
|
||||||
|
}, [stagedQuery, currentStagedQueryData]);
|
||||||
|
|
||||||
|
const exportDefaultQuery = useMemo(
|
||||||
|
() =>
|
||||||
|
updateAllQueriesOperators(
|
||||||
|
currentQuery || initialQueriesMap.logs,
|
||||||
|
PANEL_TYPES.TIME_SERIES,
|
||||||
|
DataSource.LOGS,
|
||||||
|
),
|
||||||
|
[currentQuery, updateAllQueriesOperators],
|
||||||
);
|
);
|
||||||
|
|
||||||
const tabsItems: TabsProps['items'] = useMemo(
|
const listChartData = useGetExplorerQueryRange(
|
||||||
() => [
|
listChartQuery,
|
||||||
{
|
PANEL_TYPES.TIME_SERIES,
|
||||||
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 { data, isFetching, isError } = useGetExplorerQueryRange(
|
||||||
|
requestData,
|
||||||
|
panelType,
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
enabled: !isLimit,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSetActiveLog = useCallback((nextActiveLog: ILog) => {
|
||||||
|
setActiveLog(nextActiveLog);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleClearActiveLog = useCallback(() => {
|
||||||
|
setActiveLog(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleChangeView = useCallback(
|
const handleChangeView = useCallback(
|
||||||
(newPanelType: string) => {
|
(newPanelType: string) => {
|
||||||
if (newPanelType === panelType) return;
|
if (newPanelType === panelType) return;
|
||||||
@ -101,7 +183,9 @@ function LogsExplorerViews(): JSX.Element {
|
|||||||
DataSource.LOGS,
|
DataSource.LOGS,
|
||||||
);
|
);
|
||||||
|
|
||||||
redirectWithQueryBuilderData(query, { [PANEL_TYPES_QUERY]: newPanelType });
|
redirectWithQueryBuilderData(query, {
|
||||||
|
[queryParamNamesMap.panelTypes]: newPanelType,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
currentQuery,
|
currentQuery,
|
||||||
@ -111,23 +195,325 @@ function LogsExplorerViews(): JSX.Element {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getRequestData = useCallback(
|
||||||
|
(
|
||||||
|
query: Query | null,
|
||||||
|
params: { page: number; log: ILog | null; pageSize: number },
|
||||||
|
): Query | null => {
|
||||||
|
if (!query) return null;
|
||||||
|
|
||||||
|
const paginateData = getPaginationQueryData({
|
||||||
|
currentStagedQueryData,
|
||||||
|
listItemId: params.log ? params.log.id : null,
|
||||||
|
orderByTimestamp,
|
||||||
|
page: params.page,
|
||||||
|
pageSize: params.pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data: Query = {
|
||||||
|
...query,
|
||||||
|
builder: {
|
||||||
|
...query.builder,
|
||||||
|
queryData: query.builder.queryData.map((item) => ({
|
||||||
|
...item,
|
||||||
|
...paginateData,
|
||||||
|
pageSize: params.pageSize,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
[currentStagedQueryData, orderByTimestamp],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddToQuery = useCallback(
|
||||||
|
(fieldKey: string, fieldValue: string, operator: string): void => {
|
||||||
|
const keysAutocomplete: BaseAutocompleteData[] =
|
||||||
|
queryClient.getQueryData<SuccessResponse<IQueryAutocompleteResponse>>(
|
||||||
|
[QueryBuilderKeys.GET_AGGREGATE_KEYS],
|
||||||
|
{ exact: false },
|
||||||
|
)?.payload.attributeKeys || [];
|
||||||
|
|
||||||
|
const existAutocompleteKey = chooseAutocompleteFromCustomValue(
|
||||||
|
keysAutocomplete,
|
||||||
|
fieldKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentOperator =
|
||||||
|
Object.keys(OPERATORS).find((op) => op === operator) || '';
|
||||||
|
|
||||||
|
const nextQuery: Query = {
|
||||||
|
...currentQuery,
|
||||||
|
builder: {
|
||||||
|
...currentQuery.builder,
|
||||||
|
queryData: currentQuery.builder.queryData.map((item) => ({
|
||||||
|
...item,
|
||||||
|
filters: {
|
||||||
|
...item.filters,
|
||||||
|
items: [
|
||||||
|
...item.filters.items.filter(
|
||||||
|
(item) => item.key?.id !== existAutocompleteKey.id,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: uuid(),
|
||||||
|
key: existAutocompleteKey,
|
||||||
|
op: currentOperator,
|
||||||
|
value: fieldValue,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
redirectWithQueryBuilderData(nextQuery);
|
||||||
|
},
|
||||||
|
[currentQuery, queryClient, redirectWithQueryBuilderData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEndReached = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
if (isLimit) return;
|
||||||
|
if (logs.length < pageSize) return;
|
||||||
|
|
||||||
|
const lastLog = logs[index];
|
||||||
|
|
||||||
|
const limit = currentStagedQueryData?.limit;
|
||||||
|
|
||||||
|
const nextLogsLenth = logs.length + pageSize;
|
||||||
|
|
||||||
|
const nextPageSize =
|
||||||
|
limit && nextLogsLenth >= limit ? limit - logs.length : pageSize;
|
||||||
|
|
||||||
|
if (!stagedQuery) return;
|
||||||
|
|
||||||
|
const newRequestData = getRequestData(stagedQuery, {
|
||||||
|
page: page + 1,
|
||||||
|
log: orderByTimestamp ? lastLog : null,
|
||||||
|
pageSize: nextPageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
setPage((prevPage) => prevPage + 1);
|
||||||
|
|
||||||
|
setRequestData(newRequestData);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
isLimit,
|
||||||
|
logs,
|
||||||
|
currentStagedQueryData?.limit,
|
||||||
|
pageSize,
|
||||||
|
stagedQuery,
|
||||||
|
getRequestData,
|
||||||
|
page,
|
||||||
|
orderByTimestamp,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: updateDashboard,
|
||||||
|
isLoading: isUpdateDashboardLoading,
|
||||||
|
} = useUpdateDashboard();
|
||||||
|
|
||||||
|
const handleExport = useCallback(
|
||||||
|
(dashboard: Dashboard | null): void => {
|
||||||
|
if (!dashboard) return;
|
||||||
|
|
||||||
|
const updatedDashboard = addEmptyWidgetInDashboardJSONWithQuery(
|
||||||
|
dashboard,
|
||||||
|
exportDefaultQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
updateDashboard(updatedDashboard, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
if (data.error) {
|
||||||
|
const message =
|
||||||
|
data.error === 'feature usage exceeded' ? (
|
||||||
|
<span>
|
||||||
|
Panel limit exceeded for {DataSource.LOGS} in community edition. Please
|
||||||
|
checkout our paid plans{' '}
|
||||||
|
<a
|
||||||
|
href="https://signoz.io/pricing/?utm_source=product&utm_medium=dashboard-limit"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
data.error
|
||||||
|
);
|
||||||
|
notifications.error({
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dashboardEditView = `${generatePath(ROUTES.DASHBOARD, {
|
||||||
|
dashboardId: data?.payload?.uuid,
|
||||||
|
})}/new?${QueryParams.graphType}=graph&${QueryParams.widgetId}=empty&${
|
||||||
|
queryParamNamesMap.compositeQuery
|
||||||
|
}=${encodeURIComponent(JSON.stringify(exportDefaultQuery))}`;
|
||||||
|
|
||||||
|
history.push(dashboardEditView);
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
notifications.error({
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[exportDefaultQuery, history, notifications, updateDashboard],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const shouldChangeView = isMultipleQueries || isGroupByExist;
|
const shouldChangeView = isMultipleQueries || isGroupByExist;
|
||||||
|
|
||||||
if (panelType === 'list' && shouldChangeView) {
|
if (panelType === PANEL_TYPES.LIST && shouldChangeView) {
|
||||||
handleChangeView(PANEL_TYPES.TIME_SERIES);
|
handleChangeView(PANEL_TYPES.TIME_SERIES);
|
||||||
}
|
}
|
||||||
}, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]);
|
}, [panelType, isMultipleQueries, isGroupByExist, handleChangeView]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentData = data?.payload.data.newResult.data.result || [];
|
||||||
|
if (currentData.length > 0 && currentData[0].list) {
|
||||||
|
const currentLogs: ILog[] = currentData[0].list.map((item) => ({
|
||||||
|
...item.data,
|
||||||
|
timestamp: item.timestamp,
|
||||||
|
}));
|
||||||
|
setLogs((prevLogs) => [...prevLogs, ...currentLogs]);
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
requestData?.id !== stagedQuery?.id ||
|
||||||
|
currentMinTimeRef.current !== minTime
|
||||||
|
) {
|
||||||
|
const newRequestData = getRequestData(stagedQuery, {
|
||||||
|
page: 1,
|
||||||
|
log: null,
|
||||||
|
pageSize,
|
||||||
|
});
|
||||||
|
setLogs([]);
|
||||||
|
setPage(1);
|
||||||
|
setRequestData(newRequestData);
|
||||||
|
currentMinTimeRef.current = minTime;
|
||||||
|
}
|
||||||
|
}, [stagedQuery, requestData, getRequestData, pageSize, minTime]);
|
||||||
|
|
||||||
|
const tabsItems: TabsProps['items'] = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<TabLabel
|
||||||
|
label="List View"
|
||||||
|
tooltipText="Please remove attributes from Group By filter to switch to List View tab"
|
||||||
|
isDisabled={isMultipleQueries || isGroupByExist}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
key: PANEL_TYPES.LIST,
|
||||||
|
disabled: isMultipleQueries || isGroupByExist,
|
||||||
|
children: (
|
||||||
|
<LogsExplorerList
|
||||||
|
isLoading={isFetching}
|
||||||
|
currentStagedQueryData={currentStagedQueryData}
|
||||||
|
logs={logs}
|
||||||
|
onOpenDetailedView={handleSetActiveLog}
|
||||||
|
onEndReached={handleEndReached}
|
||||||
|
onExpand={handleSetActiveLog}
|
||||||
|
onAddToQuery={handleAddToQuery}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <TabLabel label="Time Series" isDisabled={false} />,
|
||||||
|
key: PANEL_TYPES.TIME_SERIES,
|
||||||
|
children: (
|
||||||
|
<TimeSeriesView isLoading={isFetching} data={data} isError={isError} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// TODO: temporary hide table view
|
||||||
|
// {
|
||||||
|
// label: 'Table',
|
||||||
|
// key: PANEL_TYPES.TABLE,
|
||||||
|
// children: (
|
||||||
|
// <LogsExplorerTable
|
||||||
|
// data={data?.payload.data.newResult.data.result || []}
|
||||||
|
// isLoading={isFetching}
|
||||||
|
// />
|
||||||
|
// ),
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
isMultipleQueries,
|
||||||
|
isGroupByExist,
|
||||||
|
isFetching,
|
||||||
|
currentStagedQueryData,
|
||||||
|
logs,
|
||||||
|
handleSetActiveLog,
|
||||||
|
handleEndReached,
|
||||||
|
handleAddToQuery,
|
||||||
|
data,
|
||||||
|
isError,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const chartData = useMemo(() => {
|
||||||
|
if (!stagedQuery) return [];
|
||||||
|
|
||||||
|
if (panelType === PANEL_TYPES.LIST) {
|
||||||
|
if (
|
||||||
|
listChartData &&
|
||||||
|
listChartData.data &&
|
||||||
|
listChartData.data.payload.data.result.length > 0
|
||||||
|
) {
|
||||||
|
return listChartData.data.payload.data.result;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data || data.payload.data.result.length === 0) return [];
|
||||||
|
|
||||||
|
const isGroupByExist = stagedQuery.builder.queryData.some(
|
||||||
|
(queryData) => queryData.groupBy.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return isGroupByExist
|
||||||
|
? data.payload.data.result
|
||||||
|
: [data.payload.data.result[0]];
|
||||||
|
}, [stagedQuery, data, panelType, listChartData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
|
<LogsExplorerChart isLoading={isFetching} data={chartData} />
|
||||||
|
{stagedQuery && (
|
||||||
|
<ActionsWrapper>
|
||||||
|
<ExportPanel
|
||||||
|
query={exportDefaultQuery}
|
||||||
|
isLoading={isUpdateDashboardLoading}
|
||||||
|
onExport={handleExport}
|
||||||
|
/>
|
||||||
|
</ActionsWrapper>
|
||||||
|
)}
|
||||||
<TabsStyled
|
<TabsStyled
|
||||||
items={tabsItems}
|
items={tabsItems}
|
||||||
defaultActiveKey={panelType || PANEL_TYPES.LIST}
|
defaultActiveKey={panelType || PANEL_TYPES.LIST}
|
||||||
activeKey={panelType || PANEL_TYPES.LIST}
|
activeKey={panelType || PANEL_TYPES.LIST}
|
||||||
onChange={handleChangeView}
|
onChange={handleChangeView}
|
||||||
|
destroyInactiveTabPane
|
||||||
/>
|
/>
|
||||||
</div>
|
<LogDetail
|
||||||
|
log={activeLog}
|
||||||
|
onClose={handleClearActiveLog}
|
||||||
|
onAddToQuery={handleAddToQuery}
|
||||||
|
onClickActionItem={handleAddToQuery}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,13 +4,17 @@ import ListLogView from 'components/Logs/ListLogView';
|
|||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
import LogsTableView from 'components/Logs/TableView';
|
import LogsTableView from 'components/Logs/TableView';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
import { contentStyle } from 'container/Trace/Search/config';
|
import { contentStyle } from 'container/Trace/Search/config';
|
||||||
import useFontFaceObserver from 'hooks/useFontObserver';
|
import useFontFaceObserver from 'hooks/useFontObserver';
|
||||||
|
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
|
||||||
import { memo, useCallback, useMemo } from 'react';
|
import { memo, useCallback, useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
import { Virtuoso } from 'react-virtuoso';
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
// interfaces
|
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
|
// interfaces
|
||||||
|
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
import { ILogsReducer } from 'types/reducer/logs';
|
import { ILogsReducer } from 'types/reducer/logs';
|
||||||
|
|
||||||
@ -28,6 +32,10 @@ type LogsTableProps = {
|
|||||||
function LogsTable(props: LogsTableProps): JSX.Element {
|
function LogsTable(props: LogsTableProps): JSX.Element {
|
||||||
const { viewMode, onClickExpand, linesPerRow } = props;
|
const { viewMode, onClickExpand, linesPerRow } = props;
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
useFontFaceObserver(
|
useFontFaceObserver(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@ -44,6 +52,7 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
|||||||
const {
|
const {
|
||||||
logs,
|
logs,
|
||||||
fields: { selected },
|
fields: { selected },
|
||||||
|
searchFilter: { queryString },
|
||||||
isLoading,
|
isLoading,
|
||||||
liveTail,
|
liveTail,
|
||||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||||
@ -58,6 +67,30 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
|||||||
liveTail,
|
liveTail,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const handleOpenDetailedView = useCallback(
|
||||||
|
(logData: ILog) => {
|
||||||
|
dispatch({
|
||||||
|
type: SET_DETAILED_LOG_DATA,
|
||||||
|
payload: logData,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[dispatch],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddToQuery = useCallback(
|
||||||
|
(fieldKey: string, fieldValue: string, operator: string) => {
|
||||||
|
const updatedQueryString = getGeneratedFilterQueryString(
|
||||||
|
fieldKey,
|
||||||
|
fieldValue,
|
||||||
|
operator,
|
||||||
|
queryString,
|
||||||
|
);
|
||||||
|
|
||||||
|
history.replace(`${ROUTES.LOGS}?q=${updatedQueryString}`);
|
||||||
|
},
|
||||||
|
[history, queryString],
|
||||||
|
);
|
||||||
|
|
||||||
const getItemContent = useCallback(
|
const getItemContent = useCallback(
|
||||||
(index: number): JSX.Element => {
|
(index: number): JSX.Element => {
|
||||||
const log = logs[index];
|
const log = logs[index];
|
||||||
@ -73,9 +106,25 @@ function LogsTable(props: LogsTableProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ListLogView key={log.id} logData={log} />;
|
return (
|
||||||
|
<ListLogView
|
||||||
|
key={log.id}
|
||||||
|
logData={log}
|
||||||
|
selectedFields={selected}
|
||||||
|
onOpenDetailedView={handleOpenDetailedView}
|
||||||
|
onAddToQuery={handleAddToQuery}
|
||||||
|
/>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[logs, linesPerRow, viewMode, onClickExpand],
|
[
|
||||||
|
logs,
|
||||||
|
viewMode,
|
||||||
|
selected,
|
||||||
|
linesPerRow,
|
||||||
|
onClickExpand,
|
||||||
|
handleOpenDetailedView,
|
||||||
|
handleAddToQuery,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderContent = useMemo(() => {
|
const renderContent = useMemo(() => {
|
||||||
|
@ -1,5 +1,13 @@
|
|||||||
|
import { Typography } from 'antd';
|
||||||
|
import getServiceOverview from 'api/metrics/getServiceOverview';
|
||||||
|
import getTopLevelOperations, {
|
||||||
|
ServiceDataProps,
|
||||||
|
} from 'api/metrics/getTopLevelOperations';
|
||||||
|
import getTopOperations from 'api/metrics/getTopOperations';
|
||||||
|
import axios from 'axios';
|
||||||
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
|
import { ActiveElement, Chart, ChartData, ChartEvent } from 'chart.js';
|
||||||
import Graph from 'components/Graph';
|
import Graph from 'components/Graph';
|
||||||
|
import Spinner from 'components/Spinner';
|
||||||
import { QueryParams } from 'constants/query';
|
import { QueryParams } from 'constants/query';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import FullView from 'container/GridGraphLayout/Graph/FullView/index.metricsBuilder';
|
import FullView from 'container/GridGraphLayout/Graph/FullView/index.metricsBuilder';
|
||||||
@ -12,16 +20,22 @@ import {
|
|||||||
} from 'hooks/useResourceAttribute/utils';
|
} from 'hooks/useResourceAttribute/utils';
|
||||||
import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond';
|
import convertToNanoSecondsToSecond from 'lib/convertToNanoSecondsToSecond';
|
||||||
import { colors } from 'lib/getRandomColor';
|
import { colors } from 'lib/getRandomColor';
|
||||||
|
import getStep from 'lib/getStep';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import { useCallback, useMemo, useState } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
import { useQueries, UseQueryResult } from 'react-query';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
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 { PayloadProps } from 'types/api/metrics/getServiceOverview';
|
||||||
|
import { PayloadProps as PayloadPropsTopOpertions } from 'types/api/metrics/getTopOperations';
|
||||||
import { EQueryType } from 'types/common/dashboard';
|
import { EQueryType } from 'types/common/dashboard';
|
||||||
import MetricReducer from 'types/reducer/metrics';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
import { Tags } from 'types/reducer/trace';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
import { SOMETHING_WENT_WRONG } from '../../../constants/api';
|
||||||
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
import { getWidgetQueryBuilder } from '../MetricsApplication.factory';
|
||||||
import {
|
import {
|
||||||
errorPercentage,
|
errorPercentage,
|
||||||
@ -37,9 +51,17 @@ import {
|
|||||||
} from './util';
|
} from './util';
|
||||||
|
|
||||||
function Application(): JSX.Element {
|
function Application(): JSX.Element {
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
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();
|
||||||
|
const { queries } = useResourceAttribute();
|
||||||
|
const selectedTags = useMemo(
|
||||||
|
() => (convertRawQueriesToTraceSelectedTags(queries) as Tags[]) || [],
|
||||||
|
[queries],
|
||||||
|
);
|
||||||
|
|
||||||
const handleSetTimeStamp = useCallback((selectTime: number) => {
|
const handleSetTimeStamp = useCallback((selectTime: number) => {
|
||||||
setSelectedTimeStamp(selectTime);
|
setSelectedTimeStamp(selectTime);
|
||||||
@ -64,12 +86,53 @@ function Application(): JSX.Element {
|
|||||||
[handleSetTimeStamp],
|
[handleSetTimeStamp],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { topOperations, serviceOverview, topLevelOperations } = useSelector<
|
const queryResult = useQueries<
|
||||||
AppState,
|
[
|
||||||
MetricReducer
|
UseQueryResult<PayloadProps>,
|
||||||
>((state) => state.metrics);
|
UseQueryResult<PayloadPropsTopOpertions>,
|
||||||
|
UseQueryResult<ServiceDataProps>,
|
||||||
|
]
|
||||||
|
>([
|
||||||
|
{
|
||||||
|
queryKey: [servicename, selectedTags, minTime, maxTime],
|
||||||
|
queryFn: (): Promise<PayloadProps> =>
|
||||||
|
getServiceOverview({
|
||||||
|
service: servicename || '',
|
||||||
|
start: minTime,
|
||||||
|
end: maxTime,
|
||||||
|
step: getStep({
|
||||||
|
start: minTime,
|
||||||
|
end: maxTime,
|
||||||
|
inputFormat: 'ns',
|
||||||
|
}),
|
||||||
|
selectedTags,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
queryKey: [minTime, maxTime, servicename, selectedTags],
|
||||||
|
queryFn: (): Promise<PayloadPropsTopOpertions> =>
|
||||||
|
getTopOperations({
|
||||||
|
service: servicename || '',
|
||||||
|
start: minTime,
|
||||||
|
end: maxTime,
|
||||||
|
selectedTags,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
queryKey: [servicename, minTime, maxTime, selectedTags],
|
||||||
|
queryFn: (): Promise<ServiceDataProps> => getTopLevelOperations(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
const { queries } = useResourceAttribute();
|
const serviceOverview = queryResult[0].data;
|
||||||
|
const serviceOverviewError = queryResult[0].error;
|
||||||
|
const serviceOverviewIsError = queryResult[0].isError;
|
||||||
|
const serviceOverviewIsLoading = queryResult[0].isLoading;
|
||||||
|
const topOperations = queryResult[1].data;
|
||||||
|
const topLevelOperations = queryResult[2].data;
|
||||||
|
const topLevelOperationsError = queryResult[2].error;
|
||||||
|
const topLevelOperationsIsError = queryResult[2].isError;
|
||||||
|
const topLevelOperationsIsLoading = queryResult[2].isLoading;
|
||||||
|
|
||||||
const selectedTraceTags: string = JSON.stringify(
|
const selectedTraceTags: string = JSON.stringify(
|
||||||
convertRawQueriesToTraceSelectedTags(queries) || [],
|
convertRawQueriesToTraceSelectedTags(queries) || [],
|
||||||
@ -89,7 +152,9 @@ function Application(): JSX.Element {
|
|||||||
builder: operationPerSec({
|
builder: operationPerSec({
|
||||||
servicename,
|
servicename,
|
||||||
tagFilterItems,
|
tagFilterItems,
|
||||||
topLevelOperations,
|
topLevelOperations: topLevelOperations
|
||||||
|
? topLevelOperations[servicename || '']
|
||||||
|
: [],
|
||||||
}),
|
}),
|
||||||
clickhouse_sql: [],
|
clickhouse_sql: [],
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
@ -105,7 +170,9 @@ function Application(): JSX.Element {
|
|||||||
builder: errorPercentage({
|
builder: errorPercentage({
|
||||||
servicename,
|
servicename,
|
||||||
tagFilterItems,
|
tagFilterItems,
|
||||||
topLevelOperations,
|
topLevelOperations: topLevelOperations
|
||||||
|
? topLevelOperations[servicename || '']
|
||||||
|
: [],
|
||||||
}),
|
}),
|
||||||
clickhouse_sql: [],
|
clickhouse_sql: [],
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
@ -158,8 +225,12 @@ function Application(): JSX.Element {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const dataSets = useMemo(
|
const dataSets = useMemo(() => {
|
||||||
() => [
|
if (!serviceOverview) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
data: serviceOverview.map((e) =>
|
data: serviceOverview.map((e) =>
|
||||||
parseFloat(convertToNanoSecondsToSecond(e.p99)),
|
parseFloat(convertToNanoSecondsToSecond(e.p99)),
|
||||||
@ -178,19 +249,25 @@ function Application(): JSX.Element {
|
|||||||
),
|
),
|
||||||
...generalChartDataProperties('p50 Latency', 2),
|
...generalChartDataProperties('p50 Latency', 2),
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
[generalChartDataProperties, serviceOverview],
|
}, [generalChartDataProperties, serviceOverview]);
|
||||||
);
|
|
||||||
|
|
||||||
const data = useMemo(
|
const data = useMemo(() => {
|
||||||
() => ({
|
if (!serviceOverview) {
|
||||||
|
return {
|
||||||
|
datasets: [],
|
||||||
|
labels: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
datasets: dataSets,
|
datasets: dataSets,
|
||||||
labels: serviceOverview.map(
|
labels: serviceOverview.map(
|
||||||
(e) => new Date(parseFloat(convertToNanoSecondsToSecond(e.timestamp))),
|
(e) => new Date(parseFloat(convertToNanoSecondsToSecond(e.timestamp))),
|
||||||
),
|
),
|
||||||
}),
|
};
|
||||||
[serviceOverview, dataSets],
|
}, [serviceOverview, dataSets]);
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row gutter={24}>
|
<Row gutter={24}>
|
||||||
@ -208,7 +285,19 @@ function Application(): JSX.Element {
|
|||||||
View Traces
|
View Traces
|
||||||
</Button>
|
</Button>
|
||||||
<Card>
|
<Card>
|
||||||
|
{serviceOverviewIsError ? (
|
||||||
|
<Typography>
|
||||||
|
{axios.isAxiosError(serviceOverviewError)
|
||||||
|
? serviceOverviewError.response?.data
|
||||||
|
: SOMETHING_WENT_WRONG}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<GraphTitle>Latency</GraphTitle>
|
<GraphTitle>Latency</GraphTitle>
|
||||||
|
{serviceOverviewIsLoading && (
|
||||||
|
<Spinner size="large" tip="Loading..." height="40vh" />
|
||||||
|
)}
|
||||||
|
{!serviceOverviewIsLoading && (
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<Graph
|
<Graph
|
||||||
animate={false}
|
animate={false}
|
||||||
@ -220,6 +309,9 @@ function Application(): JSX.Element {
|
|||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
/>
|
/>
|
||||||
</GraphContainer>
|
</GraphContainer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
@ -237,6 +329,14 @@ function Application(): JSX.Element {
|
|||||||
View Traces
|
View Traces
|
||||||
</Button>
|
</Button>
|
||||||
<Card>
|
<Card>
|
||||||
|
{topLevelOperationsIsError ? (
|
||||||
|
<Typography>
|
||||||
|
{axios.isAxiosError(topLevelOperationsError)
|
||||||
|
? topLevelOperationsError.response?.data
|
||||||
|
: SOMETHING_WENT_WRONG}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<GraphTitle>Rate (ops/s)</GraphTitle>
|
<GraphTitle>Rate (ops/s)</GraphTitle>
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<FullView
|
<FullView
|
||||||
@ -246,8 +346,11 @@ function Application(): JSX.Element {
|
|||||||
widget={operationPerSecWidget}
|
widget={operationPerSecWidget}
|
||||||
yAxisUnit="ops"
|
yAxisUnit="ops"
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
|
isDependedDataLoaded={topLevelOperationsIsLoading}
|
||||||
/>
|
/>
|
||||||
</GraphContainer>
|
</GraphContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@ -265,6 +368,14 @@ function Application(): JSX.Element {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
|
{topLevelOperationsIsError ? (
|
||||||
|
<Typography>
|
||||||
|
{axios.isAxiosError(topLevelOperationsError)
|
||||||
|
? topLevelOperationsError.response?.data
|
||||||
|
: SOMETHING_WENT_WRONG}
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<GraphTitle>Error Percentage</GraphTitle>
|
<GraphTitle>Error Percentage</GraphTitle>
|
||||||
<GraphContainer>
|
<GraphContainer>
|
||||||
<FullView
|
<FullView
|
||||||
@ -274,14 +385,17 @@ function Application(): JSX.Element {
|
|||||||
widget={errorPercentageWidget}
|
widget={errorPercentageWidget}
|
||||||
yAxisUnit="%"
|
yAxisUnit="%"
|
||||||
onDragSelect={onDragSelect}
|
onDragSelect={onDragSelect}
|
||||||
|
isDependedDataLoaded={topLevelOperationsIsLoading}
|
||||||
/>
|
/>
|
||||||
</GraphContainer>
|
</GraphContainer>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Card>
|
<Card>
|
||||||
<TopOperationsTable data={topOperations} />
|
<TopOperationsTable data={topOperations || []} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
@ -1,90 +0,0 @@
|
|||||||
import RouteTab from 'components/RouteTab';
|
|
||||||
import ROUTES from 'constants/routes';
|
|
||||||
import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
|
|
||||||
import history from 'lib/history';
|
|
||||||
import { memo, useMemo } from 'react';
|
|
||||||
import { generatePath, useParams } from 'react-router-dom';
|
|
||||||
import { useLocation } from 'react-use';
|
|
||||||
|
|
||||||
import DBCall from './Tabs/DBCall';
|
|
||||||
import External from './Tabs/External';
|
|
||||||
import Overview from './Tabs/Overview';
|
|
||||||
|
|
||||||
function OverViewTab(): JSX.Element {
|
|
||||||
return <Overview />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function DbCallTab(): JSX.Element {
|
|
||||||
return <DBCall />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ExternalTab(): JSX.Element {
|
|
||||||
return <External />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ServiceMetrics(): JSX.Element {
|
|
||||||
const { search } = useLocation();
|
|
||||||
const { servicename } = useParams<{ servicename: string }>();
|
|
||||||
|
|
||||||
const searchParams = new URLSearchParams(search);
|
|
||||||
const tab = searchParams.get('tab');
|
|
||||||
|
|
||||||
const overMetrics = 'Overview Metrics';
|
|
||||||
const dbCallMetrics = 'Database Calls';
|
|
||||||
const externalMetrics = 'External Calls';
|
|
||||||
|
|
||||||
const getActiveKey = (): string => {
|
|
||||||
switch (tab) {
|
|
||||||
case null: {
|
|
||||||
return overMetrics;
|
|
||||||
}
|
|
||||||
case dbCallMetrics: {
|
|
||||||
return dbCallMetrics;
|
|
||||||
}
|
|
||||||
case externalMetrics: {
|
|
||||||
return externalMetrics;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return overMetrics;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeKey = getActiveKey();
|
|
||||||
|
|
||||||
const routes = useMemo(
|
|
||||||
() => [
|
|
||||||
{
|
|
||||||
Component: OverViewTab,
|
|
||||||
name: overMetrics,
|
|
||||||
route: `${generatePath(ROUTES.SERVICE_METRICS, {
|
|
||||||
servicename,
|
|
||||||
})}?tab=${overMetrics}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Component: DbCallTab,
|
|
||||||
name: dbCallMetrics,
|
|
||||||
route: `${generatePath(ROUTES.SERVICE_METRICS, {
|
|
||||||
servicename,
|
|
||||||
})}?tab=${dbCallMetrics}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Component: ExternalTab,
|
|
||||||
name: externalMetrics,
|
|
||||||
route: `${generatePath(ROUTES.SERVICE_METRICS, {
|
|
||||||
servicename,
|
|
||||||
})}?tab=${externalMetrics}`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[servicename],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ResourceAttributesFilter />
|
|
||||||
<RouteTab routes={routes} history={history} activeKey={activeKey} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(ServiceMetrics);
|
|
@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
|
||||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||||
import { COMPOSITE_QUERY } from 'constants/queryBuilderQueryNames';
|
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
@ -47,7 +47,7 @@ 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}=${encodeURIComponent(
|
}&${queryParamNamesMap.compositeQuery}=${encodeURIComponent(
|
||||||
JSON.stringify(initialQueriesMap.metrics),
|
JSON.stringify(initialQueriesMap.metrics),
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
|
@ -26,7 +26,6 @@ function SettingsDrawer(): JSX.Element {
|
|||||||
width="70%"
|
width="70%"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
visible={visible}
|
visible={visible}
|
||||||
maskClosable={false}
|
|
||||||
>
|
>
|
||||||
<DashboardSettingsContent />
|
<DashboardSettingsContent />
|
||||||
</DrawerContainer>
|
</DrawerContainer>
|
||||||
|
@ -3,12 +3,13 @@ import TextToolTip from 'components/TextToolTip';
|
|||||||
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
import { GRAPH_TYPES } from 'container/NewDashboard/ComponentsSlider';
|
||||||
import { WidgetGraphProps } from 'container/NewWidget/types';
|
import { WidgetGraphProps } from 'container/NewWidget/types';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
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 { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||||
import useUrlQuery from 'hooks/useUrlQuery';
|
import useUrlQuery from 'hooks/useUrlQuery';
|
||||||
import { useCallback } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { connect, useSelector } from 'react-redux';
|
import { connect, useSelector } from 'react-redux';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { ThunkDispatch } from 'redux-thunk';
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
@ -101,12 +102,22 @@ function QuerySection({
|
|||||||
handleStageQuery(currentQuery);
|
handleStageQuery(currentQuery);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||||
|
const config: QueryBuilderProps['filterConfigs'] = {
|
||||||
|
stepInterval: { isHidden: false, isDisabled: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}, []);
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
key: EQueryType.QUERY_BUILDER,
|
key: EQueryType.QUERY_BUILDER,
|
||||||
label: 'Query Builder',
|
label: 'Query Builder',
|
||||||
tab: <Typography>Query Builder</Typography>,
|
tab: <Typography>Query Builder</Typography>,
|
||||||
children: <QueryBuilder panelType={selectedGraph} />,
|
children: (
|
||||||
|
<QueryBuilder panelType={selectedGraph} filterConfigs={filterConfigs} />
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: EQueryType.CLICKHOUSE,
|
key: EQueryType.CLICKHOUSE,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import { Input } from 'antd';
|
import { Input, Spin } from 'antd';
|
||||||
import Typography from 'antd/es/typography/Typography';
|
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';
|
||||||
@ -26,12 +26,17 @@ function AddColumnField({ config }: AddColumnFieldProps): JSX.Element | null {
|
|||||||
|
|
||||||
<Input.Group compact>
|
<Input.Group compact>
|
||||||
<AddColumnSelect
|
<AddColumnSelect
|
||||||
|
loading={config.isFetching}
|
||||||
size="small"
|
size="small"
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
options={config.options}
|
options={config.options}
|
||||||
value={[]}
|
value={[]}
|
||||||
onChange={config.onChange}
|
onSelect={config.onSelect}
|
||||||
|
onSearch={config.onSearch}
|
||||||
|
onFocus={config.onFocus}
|
||||||
|
onBlur={config.onBlur}
|
||||||
|
notFoundContent={config.isFetching ? <Spin size="small" /> : null}
|
||||||
/>
|
/>
|
||||||
<SearchIconWrapper $isDarkMode={isDarkMode}>
|
<SearchIconWrapper $isDarkMode={isDarkMode}>
|
||||||
<SearchOutlined />
|
<SearchOutlined />
|
||||||
|
@ -18,9 +18,9 @@ function FormatField({ config }: FormatFieldProps): JSX.Element | null {
|
|||||||
value={config.value}
|
value={config.value}
|
||||||
onChange={config.onChange}
|
onChange={config.onChange}
|
||||||
>
|
>
|
||||||
<RadioButton value="row">{t('options_menu.row')}</RadioButton>
|
<RadioButton value="raw">{t('options_menu.raw')}</RadioButton>
|
||||||
<RadioButton value="default">{t('options_menu.default')}</RadioButton>
|
<RadioButton value="list">{t('options_menu.default')}</RadioButton>
|
||||||
<RadioButton value="column">{t('options_menu.column')}</RadioButton>
|
<RadioButton value="table">{t('options_menu.column')}</RadioButton>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</FormatFieldWrapper>
|
</FormatFieldWrapper>
|
||||||
);
|
);
|
||||||
|
@ -4,6 +4,6 @@ export const URL_OPTIONS = 'options';
|
|||||||
|
|
||||||
export const defaultOptionsQuery: OptionsQuery = {
|
export const defaultOptionsQuery: OptionsQuery = {
|
||||||
selectColumns: [],
|
selectColumns: [],
|
||||||
maxLines: 0,
|
maxLines: 2,
|
||||||
format: 'default',
|
format: 'list',
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { InputNumberProps, RadioProps, SelectProps } from 'antd';
|
import { InputNumberProps, RadioProps, SelectProps } from 'antd';
|
||||||
|
import { LogViewMode } from 'container/LogsTable';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
|
|
||||||
export interface OptionsQuery {
|
export interface OptionsQuery {
|
||||||
selectColumns: BaseAutocompleteData[];
|
selectColumns: BaseAutocompleteData[];
|
||||||
maxLines: number;
|
maxLines: number;
|
||||||
format: 'default' | 'row' | 'column';
|
format: LogViewMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InitialOptions
|
export interface InitialOptions
|
||||||
@ -15,7 +16,11 @@ export interface InitialOptions
|
|||||||
export type OptionsMenuConfig = {
|
export type OptionsMenuConfig = {
|
||||||
format?: Pick<RadioProps, 'value' | 'onChange'>;
|
format?: Pick<RadioProps, 'value' | 'onChange'>;
|
||||||
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
maxLines?: Pick<InputNumberProps, 'value' | 'onChange'>;
|
||||||
addColumn?: Pick<SelectProps, 'options' | 'onChange'> & {
|
addColumn?: Pick<
|
||||||
|
SelectProps,
|
||||||
|
'options' | 'onSelect' | 'onFocus' | 'onSearch' | 'onBlur'
|
||||||
|
> & {
|
||||||
|
isFetching: boolean;
|
||||||
value: BaseAutocompleteData[];
|
value: BaseAutocompleteData[];
|
||||||
onRemove: (key: string) => void;
|
onRemove: (key: string) => void;
|
||||||
};
|
};
|
||||||
|
@ -1,66 +1,146 @@
|
|||||||
import { RadioChangeEvent } from 'antd';
|
import { RadioChangeEvent } from 'antd';
|
||||||
|
import getFromLocalstorage from 'api/browser/localstorage/get';
|
||||||
|
import setToLocalstorage from 'api/browser/localstorage/set';
|
||||||
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
||||||
|
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||||
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
import { QueryBuilderKeys } from 'constants/queryBuilder';
|
||||||
|
import useDebounce from 'hooks/useDebounce';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import useUrlQueryData from 'hooks/useUrlQueryData';
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQueries, useQuery } from 'react-query';
|
||||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||||
|
import {
|
||||||
|
BaseAutocompleteData,
|
||||||
|
IQueryAutocompleteResponse,
|
||||||
|
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
import { defaultOptionsQuery, URL_OPTIONS } from './constants';
|
import { defaultOptionsQuery, URL_OPTIONS } from './constants';
|
||||||
import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types';
|
import { InitialOptions, OptionsMenuConfig, OptionsQuery } from './types';
|
||||||
import { getInitialColumns, getOptionsFromKeys } from './utils';
|
import { getOptionsFromKeys } from './utils';
|
||||||
|
|
||||||
interface UseOptionsMenuProps {
|
interface UseOptionsMenuProps {
|
||||||
dataSource: DataSource;
|
dataSource: DataSource;
|
||||||
aggregateOperator: string;
|
aggregateOperator: string;
|
||||||
initialOptions?: InitialOptions;
|
initialOptions?: InitialOptions;
|
||||||
|
storageKey: LOCALSTORAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface UseOptionsMenu {
|
interface UseOptionsMenu {
|
||||||
isLoading: boolean;
|
|
||||||
options: OptionsQuery;
|
options: OptionsQuery;
|
||||||
config: OptionsMenuConfig;
|
config: OptionsMenuConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useOptionsMenu = ({
|
const useOptionsMenu = ({
|
||||||
|
storageKey,
|
||||||
dataSource,
|
dataSource,
|
||||||
aggregateOperator,
|
aggregateOperator,
|
||||||
initialOptions = {},
|
initialOptions = {},
|
||||||
}: UseOptionsMenuProps): UseOptionsMenu => {
|
}: UseOptionsMenuProps): UseOptionsMenu => {
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
const [isFocused, setIsFocused] = useState<boolean>(false);
|
||||||
|
const debouncedSearchText = useDebounce(searchText, 300);
|
||||||
|
|
||||||
|
const localStorageOptionsQuery = useMemo(
|
||||||
|
() => getFromLocalstorage(storageKey),
|
||||||
|
[storageKey],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialQueryParams = useMemo(
|
||||||
|
() => ({
|
||||||
|
searchText: '',
|
||||||
|
aggregateAttribute: '',
|
||||||
|
tagType: null,
|
||||||
|
dataSource,
|
||||||
|
aggregateOperator,
|
||||||
|
}),
|
||||||
|
[dataSource, aggregateOperator],
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
query: optionsQuery,
|
query: optionsQuery,
|
||||||
queryData: optionsQueryData,
|
queryData: optionsQueryData,
|
||||||
redirectWithQuery: redirectWithOptionsData,
|
redirectWithQuery: redirectWithOptionsData,
|
||||||
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS);
|
} = useUrlQueryData<OptionsQuery>(URL_OPTIONS, defaultOptionsQuery);
|
||||||
|
|
||||||
const { data, isFetched, isLoading } = useQuery(
|
const initialQueries = useMemo(
|
||||||
[QueryBuilderKeys.GET_ATTRIBUTE_KEY],
|
() =>
|
||||||
async () =>
|
initialOptions?.selectColumns?.map((column) => ({
|
||||||
|
queryKey: column,
|
||||||
|
queryFn: (): Promise<
|
||||||
|
SuccessResponse<IQueryAutocompleteResponse> | ErrorResponse
|
||||||
|
> =>
|
||||||
getAggregateKeys({
|
getAggregateKeys({
|
||||||
searchText: '',
|
...initialQueryParams,
|
||||||
dataSource,
|
searchText: column,
|
||||||
aggregateOperator,
|
|
||||||
aggregateAttribute: '',
|
|
||||||
}),
|
}),
|
||||||
|
enabled: !!column && !optionsQuery,
|
||||||
|
})) || [],
|
||||||
|
[initialOptions?.selectColumns, initialQueryParams, optionsQuery],
|
||||||
);
|
);
|
||||||
|
|
||||||
const attributeKeys = useMemo(() => data?.payload?.attributeKeys || [], [
|
const initialAttributesResult = useQueries(initialQueries);
|
||||||
data?.payload?.attributeKeys,
|
|
||||||
|
const isFetchedInitialAttributes = useMemo(
|
||||||
|
() => initialAttributesResult.every((result) => result.isFetched),
|
||||||
|
[initialAttributesResult],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialSelectedColumns = useMemo(() => {
|
||||||
|
if (!isFetchedInitialAttributes) return [];
|
||||||
|
|
||||||
|
const attributesData = initialAttributesResult?.reduce(
|
||||||
|
(acc, attributeResponse) => {
|
||||||
|
const data = attributeResponse?.data?.payload?.attributeKeys || [];
|
||||||
|
|
||||||
|
return [...acc, ...data];
|
||||||
|
},
|
||||||
|
[] as BaseAutocompleteData[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
(initialOptions.selectColumns
|
||||||
|
?.map((column) => attributesData.find(({ key }) => key === column))
|
||||||
|
.filter(Boolean) as BaseAutocompleteData[]) || []
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
isFetchedInitialAttributes,
|
||||||
|
initialOptions?.selectColumns,
|
||||||
|
initialAttributesResult,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: searchedAttributesData,
|
||||||
|
isFetching: isSearchedAttributesFetching,
|
||||||
|
} = useQuery(
|
||||||
|
[QueryBuilderKeys.GET_AGGREGATE_KEYS, debouncedSearchText, isFocused],
|
||||||
|
async () =>
|
||||||
|
getAggregateKeys({
|
||||||
|
...initialQueryParams,
|
||||||
|
searchText: debouncedSearchText,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
enabled: isFocused,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const searchedAttributeKeys = useMemo(
|
||||||
|
() => searchedAttributesData?.payload?.attributeKeys || [],
|
||||||
|
[searchedAttributesData?.payload?.attributeKeys],
|
||||||
|
);
|
||||||
|
|
||||||
const initialOptionsQuery: OptionsQuery = useMemo(
|
const initialOptionsQuery: OptionsQuery = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
...defaultOptionsQuery,
|
...defaultOptionsQuery,
|
||||||
...initialOptions,
|
...initialOptions,
|
||||||
selectColumns: initialOptions?.selectColumns
|
selectColumns: initialOptions?.selectColumns
|
||||||
? getInitialColumns(initialOptions?.selectColumns || [], attributeKeys)
|
? initialSelectedColumns
|
||||||
: defaultOptionsQuery.selectColumns,
|
: defaultOptionsQuery.selectColumns,
|
||||||
}),
|
}),
|
||||||
[initialOptions, attributeKeys],
|
[initialOptions, initialSelectedColumns],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedColumnKeys = useMemo(
|
const selectedColumnKeys = useMemo(
|
||||||
@ -68,29 +148,49 @@ const useOptionsMenu = ({
|
|||||||
[optionsQueryData],
|
[optionsQueryData],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addColumnOptions = useMemo(
|
const optionsFromAttributeKeys = useMemo(() => {
|
||||||
() => getOptionsFromKeys(attributeKeys, selectedColumnKeys),
|
const filteredAttributeKeys = searchedAttributeKeys.filter(
|
||||||
[attributeKeys, selectedColumnKeys],
|
(item) => item.key !== 'body',
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelectedColumnsChange = useCallback(
|
return getOptionsFromKeys(filteredAttributeKeys, selectedColumnKeys);
|
||||||
(value: string[]) => {
|
}, [searchedAttributeKeys, selectedColumnKeys]);
|
||||||
const newSelectedColumnKeys = [
|
|
||||||
...new Set([...selectedColumnKeys, ...value]),
|
const handleRedirectWithOptionsData = useCallback(
|
||||||
];
|
(newQueryData: OptionsQuery) => {
|
||||||
|
redirectWithOptionsData(newQueryData);
|
||||||
|
|
||||||
|
setToLocalstorage(storageKey, JSON.stringify(newQueryData));
|
||||||
|
},
|
||||||
|
[storageKey, redirectWithOptionsData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectColumns = useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
const newSelectedColumnKeys = [...new Set([...selectedColumnKeys, value])];
|
||||||
const newSelectedColumns = newSelectedColumnKeys.reduce((acc, key) => {
|
const newSelectedColumns = newSelectedColumnKeys.reduce((acc, key) => {
|
||||||
const column = attributeKeys.find(({ id }) => id === key);
|
const column = [
|
||||||
|
...searchedAttributeKeys,
|
||||||
|
...optionsQueryData.selectColumns,
|
||||||
|
].find(({ id }) => id === key);
|
||||||
|
|
||||||
if (!column) return acc;
|
if (!column) return acc;
|
||||||
return [...acc, column];
|
return [...acc, column];
|
||||||
}, [] as BaseAutocompleteData[]);
|
}, [] as BaseAutocompleteData[]);
|
||||||
|
|
||||||
redirectWithOptionsData({
|
const optionsData: OptionsQuery = {
|
||||||
...defaultOptionsQuery,
|
...optionsQueryData,
|
||||||
selectColumns: newSelectedColumns,
|
selectColumns: newSelectedColumns,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
handleRedirectWithOptionsData(optionsData);
|
||||||
},
|
},
|
||||||
[attributeKeys, selectedColumnKeys, redirectWithOptionsData],
|
[
|
||||||
|
searchedAttributeKeys,
|
||||||
|
selectedColumnKeys,
|
||||||
|
optionsQueryData,
|
||||||
|
handleRedirectWithOptionsData,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRemoveSelectedColumn = useCallback(
|
const handleRemoveSelectedColumn = useCallback(
|
||||||
@ -99,63 +199,88 @@ const useOptionsMenu = ({
|
|||||||
({ id }) => id !== columnKey,
|
({ id }) => id !== columnKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!newSelectedColumns.length) {
|
if (!newSelectedColumns.length && dataSource !== DataSource.LOGS) {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
message: 'There must be at least one selected column',
|
message: 'There must be at least one selected column',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
redirectWithOptionsData({
|
const optionsData: OptionsQuery = {
|
||||||
...defaultOptionsQuery,
|
...optionsQueryData,
|
||||||
selectColumns: newSelectedColumns,
|
selectColumns: newSelectedColumns,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
handleRedirectWithOptionsData(optionsData);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[optionsQueryData, notifications, redirectWithOptionsData],
|
[dataSource, notifications, optionsQueryData, handleRedirectWithOptionsData],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFormatChange = useCallback(
|
const handleFormatChange = useCallback(
|
||||||
(event: RadioChangeEvent) => {
|
(event: RadioChangeEvent) => {
|
||||||
redirectWithOptionsData({
|
const optionsData: OptionsQuery = {
|
||||||
...defaultOptionsQuery,
|
...optionsQueryData,
|
||||||
format: event.target.value,
|
format: event.target.value,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
handleRedirectWithOptionsData(optionsData);
|
||||||
},
|
},
|
||||||
[redirectWithOptionsData],
|
[handleRedirectWithOptionsData, optionsQueryData],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMaxLinesChange = useCallback(
|
const handleMaxLinesChange = useCallback(
|
||||||
(value: string | number | null) => {
|
(value: string | number | null) => {
|
||||||
redirectWithOptionsData({
|
const optionsData: OptionsQuery = {
|
||||||
...defaultOptionsQuery,
|
...optionsQueryData,
|
||||||
maxLines: value as number,
|
maxLines: value as number,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
handleRedirectWithOptionsData(optionsData);
|
||||||
},
|
},
|
||||||
[redirectWithOptionsData],
|
[handleRedirectWithOptionsData, optionsQueryData],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleSearchAttribute = useCallback((value: string) => {
|
||||||
|
setSearchText(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleFocus = (): void => {
|
||||||
|
setIsFocused(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBlur = (): void => {
|
||||||
|
setIsFocused(false);
|
||||||
|
setSearchText('');
|
||||||
|
};
|
||||||
|
|
||||||
const optionsMenuConfig: Required<OptionsMenuConfig> = useMemo(
|
const optionsMenuConfig: Required<OptionsMenuConfig> = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
addColumn: {
|
addColumn: {
|
||||||
|
isFetching: isSearchedAttributesFetching,
|
||||||
value: optionsQueryData?.selectColumns || defaultOptionsQuery.selectColumns,
|
value: optionsQueryData?.selectColumns || defaultOptionsQuery.selectColumns,
|
||||||
options: addColumnOptions || [],
|
options: optionsFromAttributeKeys || [],
|
||||||
onChange: handleSelectedColumnsChange,
|
onFocus: handleFocus,
|
||||||
|
onBlur: handleBlur,
|
||||||
|
onSelect: handleSelectColumns,
|
||||||
onRemove: handleRemoveSelectedColumn,
|
onRemove: handleRemoveSelectedColumn,
|
||||||
|
onSearch: handleSearchAttribute,
|
||||||
},
|
},
|
||||||
format: {
|
format: {
|
||||||
value: optionsQueryData?.format || defaultOptionsQuery.format,
|
value: optionsQueryData.format || defaultOptionsQuery.format,
|
||||||
onChange: handleFormatChange,
|
onChange: handleFormatChange,
|
||||||
},
|
},
|
||||||
maxLines: {
|
maxLines: {
|
||||||
value: optionsQueryData?.maxLines || defaultOptionsQuery.maxLines,
|
value: optionsQueryData.maxLines || defaultOptionsQuery.maxLines,
|
||||||
onChange: handleMaxLinesChange,
|
onChange: handleMaxLinesChange,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
addColumnOptions,
|
optionsFromAttributeKeys,
|
||||||
optionsQueryData?.maxLines,
|
optionsQueryData?.maxLines,
|
||||||
optionsQueryData?.format,
|
optionsQueryData?.format,
|
||||||
optionsQueryData?.selectColumns,
|
optionsQueryData?.selectColumns,
|
||||||
handleSelectedColumnsChange,
|
isSearchedAttributesFetching,
|
||||||
|
handleSearchAttribute,
|
||||||
|
handleSelectColumns,
|
||||||
handleRemoveSelectedColumn,
|
handleRemoveSelectedColumn,
|
||||||
handleFormatChange,
|
handleFormatChange,
|
||||||
handleMaxLinesChange,
|
handleMaxLinesChange,
|
||||||
@ -163,13 +288,22 @@ const useOptionsMenu = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (optionsQuery || !isFetched) return;
|
if (optionsQuery || !isFetchedInitialAttributes) return;
|
||||||
|
|
||||||
redirectWithOptionsData(initialOptionsQuery);
|
const nextOptionsQuery = localStorageOptionsQuery
|
||||||
}, [isFetched, optionsQuery, initialOptionsQuery, redirectWithOptionsData]);
|
? JSON.parse(localStorageOptionsQuery)
|
||||||
|
: initialOptionsQuery;
|
||||||
|
|
||||||
|
redirectWithOptionsData(nextOptionsQuery);
|
||||||
|
}, [
|
||||||
|
isFetchedInitialAttributes,
|
||||||
|
optionsQuery,
|
||||||
|
initialOptionsQuery,
|
||||||
|
localStorageOptionsQuery,
|
||||||
|
redirectWithOptionsData,
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isLoading,
|
|
||||||
options: optionsQueryData,
|
options: optionsQueryData,
|
||||||
config: optionsMenuConfig,
|
config: optionsMenuConfig,
|
||||||
};
|
};
|
||||||
|
@ -14,15 +14,3 @@ export const getOptionsFromKeys = (
|
|||||||
({ value }) => !selectedKeys.find((key) => key === value),
|
({ 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[]);
|
|
||||||
|
@ -0,0 +1,4 @@
|
|||||||
|
export type PageSizeSelectProps = {
|
||||||
|
isLoading: boolean;
|
||||||
|
isShow: boolean;
|
||||||
|
};
|
51
frontend/src/container/PageSizeSelect/index.tsx
Normal file
51
frontend/src/container/PageSizeSelect/index.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { Col, Row, Select } from 'antd';
|
||||||
|
import { queryParamNamesMap } from 'constants/queryBuilderQueryNames';
|
||||||
|
import {
|
||||||
|
defaultSelectStyle,
|
||||||
|
ITEMS_PER_PAGE_OPTIONS,
|
||||||
|
} from 'container/Controls/config';
|
||||||
|
import useUrlQueryData from 'hooks/useUrlQueryData';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { PageSizeSelectProps } from './PageSizeSelect.interfaces';
|
||||||
|
|
||||||
|
function PageSizeSelect({
|
||||||
|
isLoading,
|
||||||
|
isShow,
|
||||||
|
}: PageSizeSelectProps): JSX.Element | null {
|
||||||
|
const { redirectWithQuery, queryData: pageSize } = useUrlQueryData<number>(
|
||||||
|
queryParamNamesMap.pageSize,
|
||||||
|
ITEMS_PER_PAGE_OPTIONS[0],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChangePageSize = useCallback(
|
||||||
|
(value: number) => {
|
||||||
|
redirectWithQuery(value);
|
||||||
|
},
|
||||||
|
[redirectWithQuery],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isShow) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<Col>
|
||||||
|
<Select
|
||||||
|
style={defaultSelectStyle}
|
||||||
|
loading={isLoading}
|
||||||
|
value={pageSize}
|
||||||
|
onChange={handleChangePageSize}
|
||||||
|
>
|
||||||
|
{ITEMS_PER_PAGE_OPTIONS.map((count) => (
|
||||||
|
<Select.Option
|
||||||
|
key={count}
|
||||||
|
value={count}
|
||||||
|
>{`${count} / page`}</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageSizeSelect;
|
@ -1,5 +1,6 @@
|
|||||||
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
|
import { ITEMS } from 'container/NewDashboard/ComponentsSlider/menuItems';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
export type QueryBuilderConfig =
|
export type QueryBuilderConfig =
|
||||||
@ -13,4 +14,7 @@ export type QueryBuilderProps = {
|
|||||||
config?: QueryBuilderConfig;
|
config?: QueryBuilderConfig;
|
||||||
panelType: ITEMS;
|
panelType: ITEMS;
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
|
filterConfigs?: Partial<
|
||||||
|
Record<keyof IBuilderQuery, { isHidden: boolean; isDisabled: boolean }>
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
import { Col } from 'antd';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
export const ActionsWrapperStyled = styled(Col)`
|
|
||||||
padding-right: 1rem;
|
|
||||||
`;
|
|
@ -10,13 +10,12 @@ import { memo, useEffect, useMemo } from 'react';
|
|||||||
import { Formula, Query } from './components';
|
import { Formula, Query } from './components';
|
||||||
// ** Types
|
// ** Types
|
||||||
import { QueryBuilderProps } from './QueryBuilder.interfaces';
|
import { QueryBuilderProps } from './QueryBuilder.interfaces';
|
||||||
// ** Styles
|
|
||||||
import { ActionsWrapperStyled } from './QueryBuilder.styled';
|
|
||||||
|
|
||||||
export const QueryBuilder = memo(function QueryBuilder({
|
export const QueryBuilder = memo(function QueryBuilder({
|
||||||
config,
|
config,
|
||||||
panelType: newPanelType,
|
panelType: newPanelType,
|
||||||
actions,
|
actions,
|
||||||
|
filterConfigs = {},
|
||||||
}: QueryBuilderProps): JSX.Element {
|
}: QueryBuilderProps): JSX.Element {
|
||||||
const {
|
const {
|
||||||
currentQuery,
|
currentQuery,
|
||||||
@ -74,6 +73,7 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
isAvailableToDisable={isAvailableToDisableQuery}
|
isAvailableToDisable={isAvailableToDisableQuery}
|
||||||
queryVariant={config?.queryVariant || 'dropdown'}
|
queryVariant={config?.queryVariant || 'dropdown'}
|
||||||
query={query}
|
query={query}
|
||||||
|
filterConfigs={filterConfigs}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
))}
|
))}
|
||||||
@ -85,7 +85,7 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<ActionsWrapperStyled span={24}>
|
<Col span={24}>
|
||||||
<Row gutter={[20, 0]}>
|
<Row gutter={[20, 0]}>
|
||||||
<Col>
|
<Col>
|
||||||
<Button
|
<Button
|
||||||
@ -109,7 +109,7 @@ export const QueryBuilder = memo(function QueryBuilder({
|
|||||||
</Col>
|
</Col>
|
||||||
{actions}
|
{actions}
|
||||||
</Row>
|
</Row>
|
||||||
</ActionsWrapperStyled>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
export type QueryProps = {
|
export type QueryProps = {
|
||||||
@ -5,4 +6,4 @@ export type QueryProps = {
|
|||||||
isAvailableToDisable: boolean;
|
isAvailableToDisable: boolean;
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
queryVariant: 'static' | 'dropdown';
|
queryVariant: 'static' | 'dropdown';
|
||||||
};
|
} & Pick<QueryBuilderProps, 'filterConfigs'>;
|
||||||
|
@ -35,6 +35,7 @@ export const Query = memo(function Query({
|
|||||||
isAvailableToDisable,
|
isAvailableToDisable,
|
||||||
queryVariant,
|
queryVariant,
|
||||||
query,
|
query,
|
||||||
|
filterConfigs,
|
||||||
}: QueryProps): JSX.Element {
|
}: QueryProps): JSX.Element {
|
||||||
const { panelType } = useQueryBuilder();
|
const { panelType } = useQueryBuilder();
|
||||||
const {
|
const {
|
||||||
@ -47,7 +48,7 @@ export const Query = memo(function Query({
|
|||||||
handleChangeQueryData,
|
handleChangeQueryData,
|
||||||
handleChangeOperator,
|
handleChangeOperator,
|
||||||
handleDeleteQuery,
|
handleDeleteQuery,
|
||||||
} = useQueryOperations({ index, query });
|
} = useQueryOperations({ index, query, filterConfigs });
|
||||||
|
|
||||||
const handleChangeAggregateEvery = useCallback(
|
const handleChangeAggregateEvery = useCallback(
|
||||||
(value: IBuilderQuery['stepInterval']) => {
|
(value: IBuilderQuery['stepInterval']) => {
|
||||||
@ -109,6 +110,30 @@ export const Query = memo(function Query({
|
|||||||
[handleChangeQueryData],
|
[handleChangeQueryData],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderAggregateEveryFilter = useCallback(
|
||||||
|
(): JSX.Element | null =>
|
||||||
|
!filterConfigs?.stepInterval?.isHidden ? (
|
||||||
|
<Row gutter={[11, 5]}>
|
||||||
|
<Col flex="5.93rem">
|
||||||
|
<FilterLabel label="Aggregate Every" />
|
||||||
|
</Col>
|
||||||
|
<Col flex="1 1 6rem">
|
||||||
|
<AggregateEveryFilter
|
||||||
|
query={query}
|
||||||
|
disabled={filterConfigs?.stepInterval?.isDisabled || false}
|
||||||
|
onChange={handleChangeAggregateEvery}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
) : null,
|
||||||
|
[
|
||||||
|
filterConfigs?.stepInterval?.isHidden,
|
||||||
|
filterConfigs?.stepInterval?.isDisabled,
|
||||||
|
query,
|
||||||
|
handleChangeAggregateEvery,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
const renderAdditionalFilters = useCallback((): ReactNode => {
|
const renderAdditionalFilters = useCallback((): ReactNode => {
|
||||||
switch (panelType) {
|
switch (panelType) {
|
||||||
case PANEL_TYPES.TIME_SERIES: {
|
case PANEL_TYPES.TIME_SERIES: {
|
||||||
@ -149,19 +174,7 @@ export const Query = memo(function Query({
|
|||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Col span={11}>
|
<Col span={11}>{renderAggregateEveryFilter()}</Col>
|
||||||
<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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -179,19 +192,7 @@ export const Query = memo(function Query({
|
|||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={11}>
|
<Col span={11}>{renderAggregateEveryFilter()}</Col>
|
||||||
<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>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -230,21 +231,7 @@ export const Query = memo(function Query({
|
|||||||
</Row>
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
{panelType !== PANEL_TYPES.LIST && (
|
<Col span={11}>{renderAggregateEveryFilter()}</Col>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -253,10 +240,10 @@ export const Query = memo(function Query({
|
|||||||
panelType,
|
panelType,
|
||||||
query,
|
query,
|
||||||
isMetricsDataSource,
|
isMetricsDataSource,
|
||||||
handleChangeAggregateEvery,
|
|
||||||
handleChangeHavingFilter,
|
handleChangeHavingFilter,
|
||||||
handleChangeLimit,
|
handleChangeLimit,
|
||||||
handleChangeOrderByKeys,
|
handleChangeOrderByKeys,
|
||||||
|
renderAggregateEveryFilter,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -8,6 +8,7 @@ import { selectStyle } from '../QueryBuilderSearch/config';
|
|||||||
function AggregateEveryFilter({
|
function AggregateEveryFilter({
|
||||||
onChange,
|
onChange,
|
||||||
query,
|
query,
|
||||||
|
disabled,
|
||||||
}: AggregateEveryFilterProps): JSX.Element {
|
}: AggregateEveryFilterProps): JSX.Element {
|
||||||
const isMetricsDataSource = useMemo(
|
const isMetricsDataSource = useMemo(
|
||||||
() => query.dataSource === DataSource.METRICS,
|
() => query.dataSource === DataSource.METRICS,
|
||||||
@ -20,7 +21,8 @@ function AggregateEveryFilter({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDisabled = isMetricsDataSource && !query.aggregateAttribute.key;
|
const isDisabled =
|
||||||
|
(isMetricsDataSource && !query.aggregateAttribute.key) || disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<InputNumber
|
<InputNumber
|
||||||
@ -37,6 +39,7 @@ function AggregateEveryFilter({
|
|||||||
interface AggregateEveryFilterProps {
|
interface AggregateEveryFilterProps {
|
||||||
onChange: (values: number) => void;
|
onChange: (values: number) => void;
|
||||||
query: IBuilderQuery;
|
query: IBuilderQuery;
|
||||||
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AggregateEveryFilter;
|
export default AggregateEveryFilter;
|
||||||
|
@ -4,20 +4,21 @@ import { AutoComplete, Spin } from 'antd';
|
|||||||
import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
|
import { getAggregateAttribute } from 'api/queryBuilder/getAggregateAttribute';
|
||||||
import {
|
import {
|
||||||
baseAutoCompleteIdKeysOrder,
|
baseAutoCompleteIdKeysOrder,
|
||||||
idDivider,
|
|
||||||
initialAutocompleteData,
|
|
||||||
QueryBuilderKeys,
|
QueryBuilderKeys,
|
||||||
selectValueDivider,
|
selectValueDivider,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
|
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||||
import useDebounce from 'hooks/useDebounce';
|
import useDebounce from 'hooks/useDebounce';
|
||||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||||
|
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||||
|
import { getAutocompleteValueAndType } from 'lib/newQueryBuilder/getAutocompleteValueAndType';
|
||||||
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery, useQueryClient } from 'react-query';
|
||||||
|
import { SuccessResponse } from 'types/api';
|
||||||
import {
|
import {
|
||||||
AutocompleteType,
|
|
||||||
BaseAutocompleteData,
|
BaseAutocompleteData,
|
||||||
DataType,
|
IQueryAutocompleteResponse,
|
||||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { ExtendedSelectOption } from 'types/common/select';
|
import { ExtendedSelectOption } from 'types/common/select';
|
||||||
@ -32,8 +33,18 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
disabled,
|
disabled,
|
||||||
onChange,
|
onChange,
|
||||||
}: AgregatorFilterProps): JSX.Element {
|
}: AgregatorFilterProps): JSX.Element {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
|
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
|
||||||
const debouncedValue = useDebounce(query.aggregateAttribute.key, 300);
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
|
|
||||||
|
const debouncedSearchText = useMemo(() => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
|
||||||
|
const [_, value] = getAutocompleteValueAndType(searchText);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}, [searchText]);
|
||||||
|
|
||||||
|
const debouncedValue = useDebounce(debouncedSearchText, DEBOUNCE_DELAY);
|
||||||
const { isFetching } = useQuery(
|
const { isFetching } = useQuery(
|
||||||
[
|
[
|
||||||
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
|
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
|
||||||
@ -69,46 +80,95 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChangeAttribute = useCallback(
|
const handleSearchText = useCallback((text: string): void => {
|
||||||
|
setSearchText(text);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const placeholder: string =
|
||||||
|
query.dataSource === DataSource.METRICS
|
||||||
|
? `${transformToUpperCase(query.dataSource)} name`
|
||||||
|
: 'Aggregate attribute';
|
||||||
|
|
||||||
|
const getAttributesData = useCallback(
|
||||||
|
(): BaseAutocompleteData[] =>
|
||||||
|
queryClient.getQueryData<SuccessResponse<IQueryAutocompleteResponse>>([
|
||||||
|
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
|
||||||
|
debouncedValue,
|
||||||
|
query.aggregateOperator,
|
||||||
|
query.dataSource,
|
||||||
|
])?.payload.attributeKeys || [],
|
||||||
|
[debouncedValue, query.aggregateOperator, query.dataSource, queryClient],
|
||||||
|
);
|
||||||
|
|
||||||
|
const getResponseAttributes = useCallback(async () => {
|
||||||
|
const response = await queryClient.fetchQuery(
|
||||||
|
[
|
||||||
|
QueryBuilderKeys.GET_AGGREGATE_ATTRIBUTE,
|
||||||
|
searchText,
|
||||||
|
query.aggregateOperator,
|
||||||
|
query.dataSource,
|
||||||
|
],
|
||||||
|
async () =>
|
||||||
|
getAggregateAttribute({
|
||||||
|
searchText,
|
||||||
|
aggregateOperator: query.aggregateOperator,
|
||||||
|
dataSource: query.dataSource,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.payload?.attributeKeys || [];
|
||||||
|
}, [query.aggregateOperator, query.dataSource, queryClient, searchText]);
|
||||||
|
|
||||||
|
const handleChangeCustomValue = useCallback(
|
||||||
|
async (value: string, attributes: BaseAutocompleteData[]) => {
|
||||||
|
const customAttribute: BaseAutocompleteData = chooseAutocompleteFromCustomValue(
|
||||||
|
attributes,
|
||||||
|
value,
|
||||||
|
);
|
||||||
|
|
||||||
|
onChange(customAttribute);
|
||||||
|
},
|
||||||
|
[onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleBlur = useCallback(async () => {
|
||||||
|
if (searchText) {
|
||||||
|
const aggregateAttributes = await getResponseAttributes();
|
||||||
|
handleChangeCustomValue(searchText, aggregateAttributes);
|
||||||
|
}
|
||||||
|
}, [getResponseAttributes, handleChangeCustomValue, searchText]);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
(
|
(
|
||||||
value: string,
|
value: string,
|
||||||
option: ExtendedSelectOption | ExtendedSelectOption[],
|
option: ExtendedSelectOption | ExtendedSelectOption[],
|
||||||
): void => {
|
): void => {
|
||||||
const currentOption = option as ExtendedSelectOption;
|
const currentOption = option as ExtendedSelectOption;
|
||||||
|
|
||||||
|
const aggregateAttributes = getAttributesData();
|
||||||
|
|
||||||
if (currentOption.key) {
|
if (currentOption.key) {
|
||||||
const [key, dataType, type, isColumn] = currentOption.key.split(idDivider);
|
const attribute = aggregateAttributes.find(
|
||||||
const attribute: BaseAutocompleteData = {
|
(item) => item.id === currentOption.key,
|
||||||
key,
|
|
||||||
dataType: dataType as DataType,
|
|
||||||
type: type as AutocompleteType,
|
|
||||||
isColumn: isColumn === 'true',
|
|
||||||
};
|
|
||||||
|
|
||||||
onChange(attribute);
|
|
||||||
} else {
|
|
||||||
const attribute = { ...initialAutocompleteData, key: value };
|
|
||||||
|
|
||||||
onChange(attribute);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[onChange],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const value = useMemo(
|
if (attribute) {
|
||||||
() =>
|
onChange(attribute);
|
||||||
transformStringWithPrefix({
|
}
|
||||||
|
} else {
|
||||||
|
handleChangeCustomValue(value, aggregateAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchText('');
|
||||||
|
},
|
||||||
|
[getAttributesData, handleChangeCustomValue, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const value = transformStringWithPrefix({
|
||||||
str: query.aggregateAttribute.key,
|
str: query.aggregateAttribute.key,
|
||||||
prefix: query.aggregateAttribute.type || '',
|
prefix: query.aggregateAttribute.type || '',
|
||||||
condition: !query.aggregateAttribute.isColumn,
|
condition: !query.aggregateAttribute.isColumn,
|
||||||
}),
|
});
|
||||||
[query],
|
|
||||||
);
|
|
||||||
|
|
||||||
const placeholder: string =
|
|
||||||
query.dataSource === DataSource.METRICS
|
|
||||||
? `${transformToUpperCase(query.dataSource)} name`
|
|
||||||
: 'Aggregate attribute';
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoComplete
|
<AutoComplete
|
||||||
@ -116,10 +176,12 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
|||||||
style={selectStyle}
|
style={selectStyle}
|
||||||
showArrow={false}
|
showArrow={false}
|
||||||
filterOption={false}
|
filterOption={false}
|
||||||
|
onSearch={handleSearchText}
|
||||||
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
notFoundContent={isFetching ? <Spin size="small" /> : null}
|
||||||
options={optionsData}
|
options={optionsData}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={handleChangeAttribute}
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -3,22 +3,19 @@ import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
|
|||||||
// ** Constants
|
// ** Constants
|
||||||
import {
|
import {
|
||||||
idDivider,
|
idDivider,
|
||||||
initialAutocompleteData,
|
|
||||||
QueryBuilderKeys,
|
QueryBuilderKeys,
|
||||||
selectValueDivider,
|
selectValueDivider,
|
||||||
} from 'constants/queryBuilder';
|
} from 'constants/queryBuilder';
|
||||||
|
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||||
import useDebounce from 'hooks/useDebounce';
|
import useDebounce from 'hooks/useDebounce';
|
||||||
|
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
|
||||||
// ** Components
|
// ** Components
|
||||||
// ** Helpers
|
// ** Helpers
|
||||||
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
import { transformStringWithPrefix } from 'lib/query/transformStringWithPrefix';
|
||||||
import { isEqual, uniqWith } from 'lodash-es';
|
import { isEqual, uniqWith } from 'lodash-es';
|
||||||
import { memo, useCallback, useEffect, useState } from 'react';
|
import { memo, useCallback, useEffect, useState } from 'react';
|
||||||
import { useQuery } from 'react-query';
|
import { useQuery, useQueryClient } from 'react-query';
|
||||||
import {
|
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||||
AutocompleteType,
|
|
||||||
BaseAutocompleteData,
|
|
||||||
DataType,
|
|
||||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
|
||||||
import { SelectOption } from 'types/common/select';
|
import { SelectOption } from 'types/common/select';
|
||||||
|
|
||||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
@ -29,6 +26,7 @@ export const GroupByFilter = memo(function GroupByFilter({
|
|||||||
onChange,
|
onChange,
|
||||||
disabled,
|
disabled,
|
||||||
}: GroupByFilterProps): JSX.Element {
|
}: GroupByFilterProps): JSX.Element {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const [searchText, setSearchText] = useState<string>('');
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
const [optionsData, setOptionsData] = useState<SelectOption<string, string>[]>(
|
const [optionsData, setOptionsData] = useState<SelectOption<string, string>[]>(
|
||||||
[],
|
[],
|
||||||
@ -38,7 +36,7 @@ export const GroupByFilter = memo(function GroupByFilter({
|
|||||||
);
|
);
|
||||||
const [isFocused, setIsFocused] = useState<boolean>(false);
|
const [isFocused, setIsFocused] = useState<boolean>(false);
|
||||||
|
|
||||||
const debouncedValue = useDebounce(searchText, 300);
|
const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
|
||||||
|
|
||||||
const { isFetching } = useQuery(
|
const { isFetching } = useQuery(
|
||||||
[QueryBuilderKeys.GET_AGGREGATE_KEYS, debouncedValue, isFocused],
|
[QueryBuilderKeys.GET_AGGREGATE_KEYS, debouncedValue, isFocused],
|
||||||
@ -81,6 +79,28 @@ export const GroupByFilter = memo(function GroupByFilter({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getAttributeKeys = useCallback(async () => {
|
||||||
|
const response = await queryClient.fetchQuery(
|
||||||
|
[QueryBuilderKeys.GET_AGGREGATE_KEYS, searchText, isFocused],
|
||||||
|
async () =>
|
||||||
|
getAggregateKeys({
|
||||||
|
aggregateAttribute: query.aggregateAttribute.key,
|
||||||
|
dataSource: query.dataSource,
|
||||||
|
aggregateOperator: query.aggregateOperator,
|
||||||
|
searchText,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.payload?.attributeKeys || [];
|
||||||
|
}, [
|
||||||
|
isFocused,
|
||||||
|
query.aggregateAttribute.key,
|
||||||
|
query.aggregateOperator,
|
||||||
|
query.dataSource,
|
||||||
|
queryClient,
|
||||||
|
searchText,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleSearchKeys = (searchText: string): void => {
|
const handleSearchKeys = (searchText: string): void => {
|
||||||
setSearchText(searchText);
|
setSearchText(searchText);
|
||||||
};
|
};
|
||||||
@ -94,30 +114,35 @@ export const GroupByFilter = memo(function GroupByFilter({
|
|||||||
setIsFocused(true);
|
setIsFocused(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (values: SelectOption<string, string>[]): void => {
|
const handleChange = useCallback(
|
||||||
|
async (values: SelectOption<string, string>[]): Promise<void> => {
|
||||||
|
const keys = await getAttributeKeys();
|
||||||
|
|
||||||
const groupByValues: BaseAutocompleteData[] = values.map((item) => {
|
const groupByValues: BaseAutocompleteData[] = values.map((item) => {
|
||||||
const [currentValue, id] = item.value.split(selectValueDivider);
|
const [currentValue, id] = item.value.split(selectValueDivider);
|
||||||
if (id && id.includes(idDivider)) {
|
|
||||||
const [key, dataType, type, isColumn] = id.split(idDivider);
|
|
||||||
|
|
||||||
return {
|
if (id && id.includes(idDivider)) {
|
||||||
id,
|
const attribute = keys.find((item) => item.id === id);
|
||||||
key: key || currentValue,
|
const existAttribute = query.groupBy.find((item) => item.id === id);
|
||||||
dataType: (dataType as DataType) || initialAutocompleteData.dataType,
|
|
||||||
type: (type as AutocompleteType) || initialAutocompleteData.type,
|
if (attribute) {
|
||||||
isColumn: isColumn
|
return attribute;
|
||||||
? isColumn === 'true'
|
|
||||||
: initialAutocompleteData.isColumn,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...initialAutocompleteData, key: currentValue };
|
if (existAttribute) {
|
||||||
|
return existAttribute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chooseAutocompleteFromCustomValue(keys, currentValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = uniqWith(groupByValues, isEqual);
|
const result = uniqWith(groupByValues, isEqual);
|
||||||
|
|
||||||
onChange(result);
|
onChange(result);
|
||||||
};
|
},
|
||||||
|
[getAttributeKeys, onChange, query.groupBy],
|
||||||
|
);
|
||||||
|
|
||||||
const clearSearch = useCallback(() => {
|
const clearSearch = useCallback(() => {
|
||||||
setSearchText('');
|
setSearchText('');
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { InputNumber } from 'antd';
|
import { InputNumber, Tooltip } from 'antd';
|
||||||
import { useMemo } from 'react';
|
// import { useMemo } from 'react';
|
||||||
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 { selectStyle } from '../QueryBuilderSearch/config';
|
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||||
|
|
||||||
function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
|
function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
|
||||||
@ -21,21 +21,25 @@ function LimitFilter({ onChange, query }: LimitFilterProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isMetricsDataSource = useMemo(
|
// const isMetricsDataSource = useMemo(
|
||||||
() => query.dataSource === DataSource.METRICS,
|
// () => query.dataSource === DataSource.METRICS,
|
||||||
[query.dataSource],
|
// [query.dataSource],
|
||||||
);
|
// );
|
||||||
|
|
||||||
|
// const isDisabled = isMetricsDataSource && !query.aggregateAttribute.key;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Tooltip placement="top" title="coming soon">
|
||||||
<InputNumber
|
<InputNumber
|
||||||
min={1}
|
min={1}
|
||||||
type="number"
|
type="number"
|
||||||
|
readOnly
|
||||||
value={query.limit}
|
value={query.limit}
|
||||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
|
||||||
style={selectStyle}
|
style={selectStyle}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
/>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import {
|
|||||||
getLabelFromValue,
|
getLabelFromValue,
|
||||||
mapLabelValuePairs,
|
mapLabelValuePairs,
|
||||||
orderByValueDelimiter,
|
orderByValueDelimiter,
|
||||||
|
splitOrderByFromString,
|
||||||
transformToOrderByStringValues,
|
transformToOrderByStringValues,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ export function OrderByFilter({
|
|||||||
}: OrderByFilterProps): JSX.Element {
|
}: OrderByFilterProps): JSX.Element {
|
||||||
const [searchText, setSearchText] = useState<string>('');
|
const [searchText, setSearchText] = useState<string>('');
|
||||||
const [selectedValue, setSelectedValue] = useState<IOption[]>(
|
const [selectedValue, setSelectedValue] = useState<IOption[]>(
|
||||||
transformToOrderByStringValues(query.orderBy) || [],
|
transformToOrderByStringValues(query.orderBy),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, isFetching } = useQuery(
|
const { data, isFetching } = useQuery(
|
||||||
@ -115,7 +116,11 @@ export function OrderByFilter({
|
|||||||
if (!match) return { label: item.label, value: item.value };
|
if (!match) return { label: item.label, value: item.value };
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars
|
||||||
const [_, order] = match.data.flat() as string[];
|
const [_, order] = match.data.flat() as string[];
|
||||||
if (order) return { label: item.label, value: item.value };
|
if (order)
|
||||||
|
return {
|
||||||
|
label: item.label,
|
||||||
|
value: item.value,
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: `${item.value} ${FILTERS.ASC}`,
|
label: `${item.value} ${FILTERS.ASC}`,
|
||||||
@ -131,29 +136,69 @@ export function OrderByFilter({
|
|||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleChange = (values: IOption[]): void => {
|
const getValidResult = useCallback(
|
||||||
const result = getUniqValues(values);
|
(result: IOption[]): IOption[] =>
|
||||||
|
result.reduce<IOption[]>((acc, item) => {
|
||||||
|
if (item.value === FILTERS.ASC || item.value === FILTERS.DESC) return acc;
|
||||||
|
|
||||||
|
if (item.value.includes(FILTERS.ASC) || item.value.includes(FILTERS.DESC)) {
|
||||||
|
const splittedOrderBy = splitOrderByFromString(item.value);
|
||||||
|
|
||||||
|
if (splittedOrderBy) {
|
||||||
|
acc.push({
|
||||||
|
label: `${splittedOrderBy.columnName} ${splittedOrderBy.order}`,
|
||||||
|
value: `${splittedOrderBy.columnName}${orderByValueDelimiter}${splittedOrderBy.order}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.push(item);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = (values: IOption[]): void => {
|
||||||
|
const validResult = getValidResult(values);
|
||||||
|
const result = getUniqValues(validResult);
|
||||||
|
|
||||||
setSelectedValue(result);
|
|
||||||
const orderByValues: OrderByPayload[] = result.map((item) => {
|
const orderByValues: OrderByPayload[] = result.map((item) => {
|
||||||
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
|
const match = Papa.parse(item.value, { delimiter: orderByValueDelimiter });
|
||||||
|
|
||||||
if (match) {
|
if (!match) {
|
||||||
const [columnName, order] = match.data.flat() as string[];
|
|
||||||
return {
|
|
||||||
columnName: checkIfKeyPresent(columnName, query.aggregateAttribute.key)
|
|
||||||
? '#SIGNOZ_VALUE'
|
|
||||||
: columnName,
|
|
||||||
order: order ?? 'asc',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columnName: item.value,
|
columnName: item.value,
|
||||||
order: 'asc',
|
order: 'asc',
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const [columnName, order] = match.data.flat() as string[];
|
||||||
|
|
||||||
|
const columnNameValue = checkIfKeyPresent(
|
||||||
|
columnName,
|
||||||
|
query.aggregateAttribute.key,
|
||||||
|
)
|
||||||
|
? '#SIGNOZ_VALUE'
|
||||||
|
: columnName;
|
||||||
|
|
||||||
|
const orderValue = order ?? 'asc';
|
||||||
|
|
||||||
|
return {
|
||||||
|
columnName: columnNameValue,
|
||||||
|
order: orderValue,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const selectedValue: IOption[] = orderByValues.map((item) => ({
|
||||||
|
label: `${item.columnName} ${item.order}`,
|
||||||
|
value: `${item.columnName} ${item.order}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setSelectedValue(selectedValue);
|
||||||
|
|
||||||
setSearchText('');
|
setSearchText('');
|
||||||
onChange(orderByValues);
|
onChange(orderByValues);
|
||||||
};
|
};
|
||||||
|
@ -4,15 +4,31 @@ 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';
|
import { OrderByPayload } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import { FILTERS } from './config';
|
||||||
|
|
||||||
export const orderByValueDelimiter = '|';
|
export const orderByValueDelimiter = '|';
|
||||||
|
|
||||||
export const transformToOrderByStringValues = (
|
export const transformToOrderByStringValues = (
|
||||||
orderBy: OrderByPayload[],
|
orderBy: OrderByPayload[],
|
||||||
): IOption[] =>
|
): IOption[] => {
|
||||||
orderBy.map((item) => ({
|
const prepareSelectedValue: IOption[] = orderBy.reduce<IOption[]>(
|
||||||
|
(acc, item) => {
|
||||||
|
if (item.columnName === '#SIGNOZ_VALUE') return acc;
|
||||||
|
|
||||||
|
const option: IOption = {
|
||||||
label: `${item.columnName} ${item.order}`,
|
label: `${item.columnName} ${item.order}`,
|
||||||
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
|
value: `${item.columnName}${orderByValueDelimiter}${item.order}`,
|
||||||
}));
|
};
|
||||||
|
|
||||||
|
acc.push(option);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return prepareSelectedValue;
|
||||||
|
};
|
||||||
|
|
||||||
export function mapLabelValuePairs(
|
export function mapLabelValuePairs(
|
||||||
arr: BaseAutocompleteData[],
|
arr: BaseAutocompleteData[],
|
||||||
@ -52,3 +68,13 @@ export function getLabelFromValue(arr: IOption[]): string[] {
|
|||||||
export function checkIfKeyPresent(str: string, valueToCheck: string): boolean {
|
export function checkIfKeyPresent(str: string, valueToCheck: string): boolean {
|
||||||
return new RegExp(`\\(${valueToCheck}\\)`).test(str);
|
return new RegExp(`\\(${valueToCheck}\\)`).test(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function splitOrderByFromString(str: string): OrderByPayload | null {
|
||||||
|
const splittedStr = str.split(' ');
|
||||||
|
const order = splittedStr.pop() || FILTERS.ASC;
|
||||||
|
const columnName = splittedStr.join(' ');
|
||||||
|
|
||||||
|
if (!columnName) return null;
|
||||||
|
|
||||||
|
return { columnName, order };
|
||||||
|
}
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
export const PLACEHOLDER =
|
||||||
|
'Search Filter : select options from suggested values, for IN/NOT IN operators - press "Enter" after selecting options';
|
@ -18,6 +18,7 @@ import { DataSource } from 'types/common/queryBuilder';
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { selectStyle } from './config';
|
import { selectStyle } from './config';
|
||||||
|
import { PLACEHOLDER } from './constant';
|
||||||
import { StyledCheckOutlined, TypographyText } from './style';
|
import { StyledCheckOutlined, TypographyText } from './style';
|
||||||
import {
|
import {
|
||||||
getOperatorValue,
|
getOperatorValue,
|
||||||
@ -155,7 +156,7 @@ function QueryBuilderSearch({
|
|||||||
filterOption={false}
|
filterOption={false}
|
||||||
autoClearSearchValue={false}
|
autoClearSearchValue={false}
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
placeholder="Search Filter"
|
placeholder={PLACEHOLDER}
|
||||||
value={queryTags}
|
value={queryTags}
|
||||||
searchValue={searchValue}
|
searchValue={searchValue}
|
||||||
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
disabled={isMetricsDataSource && !query.aggregateAttribute.key}
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
import type { ColumnsType } from 'antd/es/table';
|
|
||||||
import { ResizeTable } from 'components/ResizeTable';
|
import { ResizeTable } from 'components/ResizeTable';
|
||||||
import dayjs from 'dayjs';
|
import { createTableColumnsFromQuery } from 'lib/query/createTableColumnsFromQuery';
|
||||||
import {
|
|
||||||
createTableColumnsFromQuery,
|
|
||||||
RowData,
|
|
||||||
} from 'lib/query/createTableColumnsFromQuery';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { QueryTableProps } from './QueryTable.intefaces';
|
import { QueryTableProps } from './QueryTable.intefaces';
|
||||||
@ -26,23 +21,11 @@ export function QueryTable({
|
|||||||
[query, queryTableData, renderActionCell],
|
[query, queryTableData, renderActionCell],
|
||||||
);
|
);
|
||||||
|
|
||||||
const modifiedColumns = useMemo(() => {
|
const filteredColumns = columns.filter((item) => item.key !== 'timestamp');
|
||||||
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
|
const tableColumns = modifyColumns
|
||||||
? modifyColumns(modifiedColumns)
|
? modifyColumns(filteredColumns)
|
||||||
: modifiedColumns;
|
: filteredColumns;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResizeTable
|
<ResizeTable
|
||||||
|
@ -11,6 +11,7 @@ function TimeSeriesView({
|
|||||||
data,
|
data,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
|
yAxisUnit,
|
||||||
}: TimeSeriesViewProps): JSX.Element {
|
}: TimeSeriesViewProps): JSX.Element {
|
||||||
const chartData = useMemo(
|
const chartData = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -32,6 +33,7 @@ function TimeSeriesView({
|
|||||||
<Graph
|
<Graph
|
||||||
animate={false}
|
animate={false}
|
||||||
data={chartData}
|
data={chartData}
|
||||||
|
yAxisUnit={yAxisUnit}
|
||||||
name="tracesExplorerGraph"
|
name="tracesExplorerGraph"
|
||||||
type="line"
|
type="line"
|
||||||
/>
|
/>
|
||||||
@ -42,12 +44,14 @@ function TimeSeriesView({
|
|||||||
|
|
||||||
interface TimeSeriesViewProps {
|
interface TimeSeriesViewProps {
|
||||||
data?: SuccessResponse<MetricRangePayloadProps>;
|
data?: SuccessResponse<MetricRangePayloadProps>;
|
||||||
|
yAxisUnit?: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isError: boolean;
|
isError: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeSeriesView.defaultProps = {
|
TimeSeriesView.defaultProps = {
|
||||||
data: undefined,
|
data: undefined,
|
||||||
|
yAxisUnit: 'short',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TimeSeriesView;
|
export default TimeSeriesView;
|
||||||
|
@ -2,23 +2,43 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
|||||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||||
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 { useMemo } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
import TimeSeriesView from './TimeSeriesView';
|
import TimeSeriesView from './TimeSeriesView';
|
||||||
|
import { convertDataValueToMs } from './utils';
|
||||||
|
|
||||||
function TimeSeriesViewContainer({
|
function TimeSeriesViewContainer({
|
||||||
dataSource = DataSource.TRACES,
|
dataSource = DataSource.TRACES,
|
||||||
}: TimeSeriesViewProps): JSX.Element {
|
}: TimeSeriesViewProps): JSX.Element {
|
||||||
const { stagedQuery, panelType } = useQueryBuilder();
|
const { stagedQuery, currentQuery, panelType } = useQueryBuilder();
|
||||||
|
|
||||||
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
|
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
|
||||||
AppState,
|
AppState,
|
||||||
GlobalReducer
|
GlobalReducer
|
||||||
>((state) => state.globalTime);
|
>((state) => state.globalTime);
|
||||||
|
|
||||||
|
const isValidToConvertToMs = useMemo(() => {
|
||||||
|
const isValid: boolean[] = [];
|
||||||
|
|
||||||
|
currentQuery.builder.queryData.forEach(
|
||||||
|
({ aggregateAttribute, aggregateOperator }) => {
|
||||||
|
const isExistDurationNanoAttribute =
|
||||||
|
aggregateAttribute.key === 'durationNano';
|
||||||
|
|
||||||
|
const isCountOperator =
|
||||||
|
aggregateOperator === 'count' || aggregateOperator === 'count_distinct';
|
||||||
|
|
||||||
|
isValid.push(!isCountOperator && isExistDurationNanoAttribute);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return isValid.every(Boolean);
|
||||||
|
}, [currentQuery]);
|
||||||
|
|
||||||
const { data, isLoading, isError } = useGetQueryRange(
|
const { data, isLoading, isError } = useGetQueryRange(
|
||||||
{
|
{
|
||||||
query: stagedQuery || initialQueriesMap[dataSource],
|
query: stagedQuery || initialQueriesMap[dataSource],
|
||||||
@ -41,7 +61,19 @@ function TimeSeriesViewContainer({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return <TimeSeriesView isError={isError} isLoading={isLoading} data={data} />;
|
const responseData = useMemo(
|
||||||
|
() => (isValidToConvertToMs ? convertDataValueToMs(data) : data),
|
||||||
|
[data, isValidToConvertToMs],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TimeSeriesView
|
||||||
|
isError={isError}
|
||||||
|
isLoading={isLoading}
|
||||||
|
data={responseData}
|
||||||
|
yAxisUnit={isValidToConvertToMs ? 'ms' : 'short'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TimeSeriesViewProps {
|
interface TimeSeriesViewProps {
|
||||||
|
26
frontend/src/container/TimeSeriesView/utils.ts
Normal file
26
frontend/src/container/TimeSeriesView/utils.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { SuccessResponse } from 'types/api/index';
|
||||||
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||||
|
import { QueryData } from 'types/api/widgets/getQuery';
|
||||||
|
|
||||||
|
export const convertDataValueToMs = (
|
||||||
|
data?: SuccessResponse<MetricRangePayloadProps>,
|
||||||
|
): SuccessResponse<MetricRangePayloadProps> | undefined => {
|
||||||
|
const convertedData = data;
|
||||||
|
|
||||||
|
const convertedResult: QueryData[] = data?.payload?.data?.result
|
||||||
|
? data.payload.data.result.map((item) => {
|
||||||
|
const values: [number, string][] = item.values.map((value) => {
|
||||||
|
const [first = 0, second = ''] = value || [];
|
||||||
|
return [first, String(Number(second) / 1000000)];
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ...item, values };
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (convertedData?.payload?.data?.result && convertedResult) {
|
||||||
|
convertedData.payload.data.result = convertedResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
return convertedData;
|
||||||
|
};
|
3
frontend/src/container/TopNav/NewExplorerCTA/config.ts
Normal file
3
frontend/src/container/TopNav/NewExplorerCTA/config.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const RIBBON_STYLES = {
|
||||||
|
top: '-0.75rem',
|
||||||
|
};
|
50
frontend/src/container/TopNav/NewExplorerCTA/index.tsx
Normal file
50
frontend/src/container/TopNav/NewExplorerCTA/index.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { CompassOutlined } from '@ant-design/icons';
|
||||||
|
import { Badge, Button } from 'antd';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import history from 'lib/history';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { RIBBON_STYLES } from './config';
|
||||||
|
|
||||||
|
function NewExplorerCTA(): JSX.Element | null {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const isTraceOrLogsExplorerPage = useMemo(
|
||||||
|
() => location.pathname === ROUTES.LOGS || location.pathname === ROUTES.TRACE,
|
||||||
|
[location.pathname],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onClickHandler = (): void => {
|
||||||
|
if (location.pathname === ROUTES.LOGS) {
|
||||||
|
history.push(ROUTES.LOGS_EXPLORER);
|
||||||
|
} else if (location.pathname === ROUTES.TRACE) {
|
||||||
|
history.push(ROUTES.TRACES_EXPLORER);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonText = useMemo(
|
||||||
|
() =>
|
||||||
|
`Try new ${ROUTES.LOGS === location.pathname ? 'Logs' : 'Traces'} Explorer`,
|
||||||
|
[location.pathname],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isTraceOrLogsExplorerPage) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge.Ribbon style={RIBBON_STYLES} text="New">
|
||||||
|
<Button
|
||||||
|
icon={<CompassOutlined />}
|
||||||
|
onClick={onClickHandler}
|
||||||
|
danger
|
||||||
|
type="primary"
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</Button>
|
||||||
|
</Badge.Ribbon>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NewExplorerCTA;
|
@ -1,4 +1,4 @@
|
|||||||
import { Col } from 'antd';
|
import { Col, Row, Space } from 'antd';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { matchPath, useHistory } from 'react-router-dom';
|
import { matchPath, useHistory } from 'react-router-dom';
|
||||||
@ -6,6 +6,7 @@ import { matchPath, useHistory } from 'react-router-dom';
|
|||||||
import ShowBreadcrumbs from './Breadcrumbs';
|
import ShowBreadcrumbs from './Breadcrumbs';
|
||||||
import DateTimeSelector from './DateTimeSelection';
|
import DateTimeSelector from './DateTimeSelection';
|
||||||
import { routesToSkip } from './DateTimeSelection/config';
|
import { routesToSkip } from './DateTimeSelection/config';
|
||||||
|
import NewExplorerCTA from './NewExplorerCTA';
|
||||||
import { Container } from './styles';
|
import { Container } from './styles';
|
||||||
|
|
||||||
function TopNav(): JSX.Element | null {
|
function TopNav(): JSX.Element | null {
|
||||||
@ -36,7 +37,15 @@ function TopNav(): JSX.Element | null {
|
|||||||
|
|
||||||
{!isRouteToSkip && (
|
{!isRouteToSkip && (
|
||||||
<Col span={8}>
|
<Col span={8}>
|
||||||
|
<Row justify="end">
|
||||||
|
<Space align="start" size={60} direction="horizontal">
|
||||||
|
<NewExplorerCTA />
|
||||||
|
|
||||||
|
<div>
|
||||||
<DateTimeSelector />
|
<DateTimeSelector />
|
||||||
|
</div>
|
||||||
|
</Space>
|
||||||
|
</Row>
|
||||||
</Col>
|
</Col>
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
Dispatch,
|
Dispatch,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
@ -24,13 +23,9 @@ function Tags({
|
|||||||
setText,
|
setText,
|
||||||
}: TagsProps): JSX.Element {
|
}: TagsProps): JSX.Element {
|
||||||
const { t } = useTranslation(['traceDetails']);
|
const { t } = useTranslation(['traceDetails']);
|
||||||
const [allRenderedTags, setAllRenderedTags] = useState(tags);
|
const [searchText, setSearchText] = useState('');
|
||||||
const isSearchVisible = useMemo(() => tags.length > 5, [tags]);
|
const isSearchVisible = useMemo(() => tags.length > 5, [tags]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setAllRenderedTags(tags);
|
|
||||||
}, [tags]);
|
|
||||||
|
|
||||||
const getLink = useCallback(
|
const getLink = useCallback(
|
||||||
(item: Record<string, string>) =>
|
(item: Record<string, string>) =>
|
||||||
`${ROUTES.TRACE}/${item.TraceId}${formUrlParams({
|
`${ROUTES.TRACE}/${item.TraceId}${formUrlParams({
|
||||||
@ -41,14 +36,12 @@ function Tags({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onChangeHandler = useCallback(
|
const onChangeHandler = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||||
(e: ChangeEvent<HTMLInputElement>): void => {
|
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
const filteredTags = tags.filter((tag) => tag.key.includes(value));
|
setSearchText(value);
|
||||||
setAllRenderedTags(filteredTags);
|
};
|
||||||
},
|
|
||||||
[tags],
|
const filteredTags = tags.filter((tag) => tag.key.includes(searchText));
|
||||||
);
|
|
||||||
|
|
||||||
if (tags.length === 0) {
|
if (tags.length === 0) {
|
||||||
return <Typography>No tags in selected span</Typography>;
|
return <Typography>No tags in selected span</Typography>;
|
||||||
@ -61,9 +54,10 @@ function Tags({
|
|||||||
placeholder={t('traceDetails:search_tags')}
|
placeholder={t('traceDetails:search_tags')}
|
||||||
allowClear
|
allowClear
|
||||||
onChange={onChangeHandler}
|
onChange={onChangeHandler}
|
||||||
|
value={searchText}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{allRenderedTags.map((tag) => (
|
{filteredTags.map((tag) => (
|
||||||
<Tag
|
<Tag
|
||||||
key={JSON.stringify(tag)}
|
key={JSON.stringify(tag)}
|
||||||
{...{
|
{...{
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
|
import { DEFAULT_PER_PAGE_OPTIONS } from 'hooks/queryPagination';
|
||||||
|
|
||||||
export const defaultSelectedColumns: string[] = [
|
export const defaultSelectedColumns: string[] = [
|
||||||
'name',
|
|
||||||
'serviceName',
|
'serviceName',
|
||||||
'responseStatusCode',
|
'name',
|
||||||
'httpMethod',
|
|
||||||
'durationNano',
|
'durationNano',
|
||||||
|
'httpMethod',
|
||||||
|
'responseStatusCode',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];
|
export const PER_PAGE_OPTIONS: number[] = [10, ...DEFAULT_PER_PAGE_OPTIONS];
|
||||||
|
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