mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-14 09:46:05 +08:00
commit
d170515d4d
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@ package.json
|
||||
deploy/docker/environment_tiny/common_test
|
||||
frontend/node_modules
|
||||
frontend/.pnp
|
||||
frontend/i18n-translations-hash.json
|
||||
*.pnp.js
|
||||
|
||||
# testing
|
||||
|
20
Makefile
20
Makefile
@ -13,6 +13,8 @@ FRONTEND_DIRECTORY ?= frontend
|
||||
QUERY_SERVICE_DIRECTORY ?= pkg/query-service
|
||||
STANDALONE_DIRECTORY ?= deploy/docker/clickhouse-setup
|
||||
SWARM_DIRECTORY ?= deploy/docker-swarm/clickhouse-setup
|
||||
LOCAL_GOOS ?= $(shell go env GOOS)
|
||||
LOCAL_GOARCH ?= $(shell go env GOARCH)
|
||||
|
||||
REPONAME ?= signoz
|
||||
DOCKER_TAG ?= latest
|
||||
@ -79,11 +81,25 @@ dev-setup:
|
||||
@echo "--> Local Setup completed"
|
||||
@echo "------------------"
|
||||
|
||||
run-local:
|
||||
@LOCAL_GOOS=$(LOCAL_GOOS) LOCAL_GOARCH=$(LOCAL_GOARCH) docker-compose -f \
|
||||
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-local.yaml \
|
||||
up --build -d
|
||||
|
||||
down-local:
|
||||
@docker-compose -f \
|
||||
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-local.yaml \
|
||||
down -v
|
||||
|
||||
run-x86:
|
||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml up -d
|
||||
@docker-compose -f \
|
||||
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-prod.yaml \
|
||||
up --build -d
|
||||
|
||||
down-x86:
|
||||
@docker-compose -f $(STANDALONE_DIRECTORY)/docker-compose.yaml down -v
|
||||
@docker-compose -f \
|
||||
$(STANDALONE_DIRECTORY)/docker-compose-core.yaml -f $(STANDALONE_DIRECTORY)/docker-compose-prod.yaml \
|
||||
down -v
|
||||
|
||||
clear-standalone-data:
|
||||
@docker run --rm -v "$(PWD)/$(STANDALONE_DIRECTORY)/data:/pwd" busybox \
|
||||
|
@ -40,7 +40,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.10.2
|
||||
image: signoz/query-service:0.11.0
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
@ -51,10 +51,12 @@ services:
|
||||
- ./data/signoz/:/var/lib/signoz/
|
||||
environment:
|
||||
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
|
||||
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
|
||||
- STORAGE=clickhouse
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-swarm
|
||||
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
|
||||
@ -68,7 +70,7 @@ services:
|
||||
- clickhouse
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.10.2
|
||||
image: signoz/frontend:0.11.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@ -81,10 +83,14 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/otelcontribcol:0.45.1-1.3
|
||||
image: signoz-otel-collector:0.55.0
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
@ -97,21 +103,15 @@ services:
|
||||
# - "14268:14268" # Jaeger thrift HTTP
|
||||
# - "55678:55678" # OpenCensus receiver
|
||||
# - "55679:55679" # zPages extension
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name={{.Node.Hostname}},os.type={{.Node.Platform.OS}},dockerswarm.service.name={{.Service.Name}},dockerswarm.task.name={{.Task.Name}}
|
||||
deploy:
|
||||
mode: replicated
|
||||
replicas: 3
|
||||
mode: global
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
resources:
|
||||
limits:
|
||||
memory: 2000m
|
||||
depends_on:
|
||||
- clickhouse
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/otelcontribcol:0.45.1-1.3
|
||||
image: signoz-otel-collector:0.55.0
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
@ -1,4 +1,29 @@
|
||||
receivers:
|
||||
filelog/dockercontainers:
|
||||
include: [ "/var/lib/docker/containers/*/*.log" ]
|
||||
start_at: end
|
||||
include_file_path: true
|
||||
include_file_name: false
|
||||
operators:
|
||||
- type: json_parser
|
||||
id: parser-docker
|
||||
output: extract_metadata_from_filepath
|
||||
timestamp:
|
||||
parse_from: attributes.time
|
||||
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
|
||||
- type: regex_parser
|
||||
id: extract_metadata_from_filepath
|
||||
regex: '^.*containers/(?P<container_id>[^_]+)/.*log$'
|
||||
parse_from: attributes["log.file.path"]
|
||||
output: parse_body
|
||||
- type: move
|
||||
id: parse_body
|
||||
from: attributes.log
|
||||
to: body
|
||||
output: time
|
||||
- type: remove
|
||||
id: time
|
||||
field: attributes.time
|
||||
opencensus:
|
||||
endpoint: 0.0.0.0:55678
|
||||
otlp/spanmetrics:
|
||||
@ -76,6 +101,16 @@ exporters:
|
||||
prometheus:
|
||||
endpoint: 0.0.0.0:8889
|
||||
# logging: {}
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/
|
||||
timeout: 5s
|
||||
sending_queue:
|
||||
queue_size: 100
|
||||
retry_on_failure:
|
||||
enabled: true
|
||||
initial_interval: 5s
|
||||
max_interval: 30s
|
||||
max_elapsed_time: 300s
|
||||
|
||||
extensions:
|
||||
health_check:
|
||||
@ -106,3 +141,7 @@ service:
|
||||
metrics/spanmetrics:
|
||||
receivers: [otlp/spanmetrics]
|
||||
exporters: [prometheus]
|
||||
logs:
|
||||
receivers: [otlp, filelog/dockercontainers]
|
||||
processors: [batch]
|
||||
exporters: [clickhouselogsexporter]
|
||||
|
108
deploy/docker/clickhouse-setup/docker-compose-core.yaml
Normal file
108
deploy/docker/clickhouse-setup/docker-compose-core.yaml
Normal file
@ -0,0 +1,108 @@
|
||||
version: "2.4"
|
||||
|
||||
services:
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:22.4.5-alpine
|
||||
container_name: clickhouse
|
||||
# ports:
|
||||
# - "9000:9000"
|
||||
# - "8123:8123"
|
||||
tty: true
|
||||
volumes:
|
||||
- ./clickhouse-config.xml:/etc/clickhouse-server/config.xml
|
||||
- ./clickhouse-users.xml:/etc/clickhouse-server/users.xml
|
||||
# - ./clickhouse-storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
- ./data/clickhouse/:/var/lib/clickhouse/
|
||||
restart: on-failure
|
||||
logging:
|
||||
options:
|
||||
max-size: 50m
|
||||
max-file: "3"
|
||||
healthcheck:
|
||||
# "clickhouse", "client", "-u ${CLICKHOUSE_USER}", "--password ${CLICKHOUSE_PASSWORD}", "-q 'SELECT 1'"
|
||||
test: ["CMD", "wget", "--spider", "-q", "localhost:8123/ping"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
alertmanager:
|
||||
container_name: alertmanager
|
||||
image: signoz/alertmanager:0.23.0-0.2
|
||||
volumes:
|
||||
- ./data/alertmanager:/data
|
||||
depends_on:
|
||||
query-service:
|
||||
condition: service_healthy
|
||||
restart: on-failure
|
||||
command:
|
||||
- --queryService.url=http://query-service:8085
|
||||
- --storage.path=/data
|
||||
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
otel-collector:
|
||||
container_name: otel-collector
|
||||
image: signoz/signoz-otel-collector:0.55.0
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
# user: root # required for reading docker container logs
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||
ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
- "4317:4317" # OTLP gRPC receiver
|
||||
- "4318:4318" # OTLP HTTP receiver
|
||||
# - "8888:8888" # OtelCollector internal metrics
|
||||
# - "8889:8889" # signoz spanmetrics exposed by the agent
|
||||
# - "9411:9411" # Zipkin port
|
||||
# - "13133:13133" # health check extension
|
||||
# - "14250:14250" # Jaeger gRPC
|
||||
# - "14268:14268" # Jaeger thrift HTTP
|
||||
# - "55678:55678" # OpenCensus receiver
|
||||
# - "55679:55679" # zPages extension
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
|
||||
otel-collector-metrics:
|
||||
container_name: otel-collector-metrics
|
||||
image: signoz/signoz-otel-collector:0.55.0
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
# ports:
|
||||
# - "1777:1777" # pprof extension
|
||||
# - "8888:8888" # OtelCollector internal metrics
|
||||
# - "13133:13133" # Health check extension
|
||||
# - "55679:55679" # zPages extension
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
|
||||
hotrod:
|
||||
image: jaegertracing/example-hotrod:1.30
|
||||
container_name: hotrod
|
||||
logging:
|
||||
options:
|
||||
max-size: 50m
|
||||
max-file: "3"
|
||||
command: ["all"]
|
||||
environment:
|
||||
- JAEGER_ENDPOINT=http://otel-collector:14268/api/traces
|
||||
|
||||
load-hotrod:
|
||||
image: "grubykarol/locust:1.2.3-python3.9-alpine3.12"
|
||||
container_name: load-hotrod
|
||||
hostname: load-hotrod
|
||||
environment:
|
||||
ATTACKED_HOST: http://hotrod:8080
|
||||
LOCUST_MODE: standalone
|
||||
NO_PROXY: standalone
|
||||
TASK_DELAY_FROM: 5
|
||||
TASK_DELAY_TO: 30
|
||||
QUIET_MODE: "${QUIET_MODE:-false}"
|
||||
LOCUST_OPTS: "--headless -u 10 -r 1"
|
||||
volumes:
|
||||
- ../common/locust-scripts:/locust
|
55
deploy/docker/clickhouse-setup/docker-compose-local.yaml
Normal file
55
deploy/docker/clickhouse-setup/docker-compose-local.yaml
Normal file
@ -0,0 +1,55 @@
|
||||
version: "2.4"
|
||||
|
||||
services:
|
||||
query-service:
|
||||
hostname: query-service
|
||||
build:
|
||||
context: "../../../pkg/query-service"
|
||||
dockerfile: "./Dockerfile"
|
||||
args:
|
||||
LDFLAGS: ""
|
||||
TARGETPLATFORM: "${LOCAL_GOOS}/${LOCAL_GOARCH}"
|
||||
container_name: query-service
|
||||
environment:
|
||||
- ClickHouseUrl=tcp://clickhouse:9000
|
||||
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
|
||||
- STORAGE=clickhouse
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
|
||||
volumes:
|
||||
- ./prometheus.yml:/root/config/prometheus.yml
|
||||
- ../dashboards:/root/config/dashboards
|
||||
- ./data/signoz/:/var/lib/signoz/
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
ports:
|
||||
- "6060:6060"
|
||||
- "8080:8080"
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: "../../../frontend"
|
||||
dockerfile: "./Dockerfile"
|
||||
args:
|
||||
TARGETOS: "${LOCAL_GOOS}"
|
||||
TARGETPLATFORM: "${LOCAL_GOARCH}"
|
||||
container_name: frontend
|
||||
environment:
|
||||
- FRONTEND_API_ENDPOINT=http://query-service:8080
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
- alertmanager
|
||||
- query-service
|
||||
ports:
|
||||
- "3301:3301"
|
||||
volumes:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
43
deploy/docker/clickhouse-setup/docker-compose-prod.yaml
Normal file
43
deploy/docker/clickhouse-setup/docker-compose-prod.yaml
Normal file
@ -0,0 +1,43 @@
|
||||
version: "2.4"
|
||||
|
||||
services:
|
||||
query-service:
|
||||
image: signoz/query-service:0.11.0
|
||||
container_name: query-service
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
# - "6060:6060" # pprof port
|
||||
# - "8080:8080" # query-service port
|
||||
volumes:
|
||||
- ./prometheus.yml:/root/config/prometheus.yml
|
||||
- ../dashboards:/root/config/dashboards
|
||||
- ./data/signoz/:/var/lib/signoz/
|
||||
environment:
|
||||
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
|
||||
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
|
||||
- STORAGE=clickhouse
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
- DEPLOYMENT_TYPE=docker-standalone-amd
|
||||
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
|
||||
restart: on-failure
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--spider", "-q", "localhost:8080/api/v1/version"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.11.0
|
||||
container_name: frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
- alertmanager
|
||||
- query-service
|
||||
ports:
|
||||
- "3301:3301"
|
||||
volumes:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
@ -39,7 +39,7 @@ services:
|
||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.10.2
|
||||
image: signoz/query-service:0.11.0
|
||||
container_name: query-service
|
||||
command: ["-config=/root/config/prometheus.yml"]
|
||||
# ports:
|
||||
@ -51,6 +51,8 @@ services:
|
||||
- ./data/signoz/:/var/lib/signoz/
|
||||
environment:
|
||||
- ClickHouseUrl=tcp://clickhouse:9000/?database=signoz_traces
|
||||
- ALERTMANAGER_API_PREFIX=http://alertmanager:9093/api/
|
||||
- SIGNOZ_LOCAL_DB_PATH=/var/lib/signoz/signoz.db
|
||||
- STORAGE=clickhouse
|
||||
- GODEBUG=netdns=go
|
||||
- TELEMETRY_ENABLED=true
|
||||
@ -66,7 +68,7 @@ services:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.10.2
|
||||
image: signoz/frontend:0.11.0
|
||||
container_name: frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@ -78,10 +80,12 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/otelcontribcol:0.45.1-1.3
|
||||
image: signoz/signoz-otel-collector:0.55.0
|
||||
command: ["--config=/etc/otel-collector-config.yaml"]
|
||||
user: root # required for reading docker container logs
|
||||
volumes:
|
||||
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
|
||||
- /var/lib/docker/containers:/var/lib/docker/containers:ro
|
||||
environment:
|
||||
- OTEL_RESOURCE_ATTRIBUTES=host.name=signoz-host,os.type=linux
|
||||
ports:
|
||||
@ -96,14 +100,13 @@ services:
|
||||
# - "14268:14268" # Jaeger thrift HTTP
|
||||
# - "55678:55678" # OpenCensus receiver
|
||||
# - "55679:55679" # zPages extension
|
||||
mem_limit: 2000m
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
clickhouse:
|
||||
condition: service_healthy
|
||||
|
||||
otel-collector-metrics:
|
||||
image: signoz/otelcontribcol:0.45.1-1.3
|
||||
image: signoz/signoz-otel-collector:0.55.0
|
||||
command: ["--config=/etc/otel-collector-metrics-config.yaml"]
|
||||
volumes:
|
||||
- ./otel-collector-metrics-config.yaml:/etc/otel-collector-metrics-config.yaml
|
||||
|
@ -1,4 +1,29 @@
|
||||
receivers:
|
||||
filelog/dockercontainers:
|
||||
include: [ "/var/lib/docker/containers/*/*.log" ]
|
||||
start_at: end
|
||||
include_file_path: true
|
||||
include_file_name: false
|
||||
operators:
|
||||
- type: json_parser
|
||||
id: parser-docker
|
||||
output: extract_metadata_from_filepath
|
||||
timestamp:
|
||||
parse_from: attributes.time
|
||||
layout: '%Y-%m-%dT%H:%M:%S.%LZ'
|
||||
- type: regex_parser
|
||||
id: extract_metadata_from_filepath
|
||||
regex: '^.*containers/(?P<container_id>[^_]+)/.*log$'
|
||||
parse_from: attributes["log.file.path"]
|
||||
output: parse_body
|
||||
- type: move
|
||||
id: parse_body
|
||||
from: attributes.log
|
||||
to: body
|
||||
output: time
|
||||
- type: remove
|
||||
id: time
|
||||
field: attributes.time
|
||||
opencensus:
|
||||
endpoint: 0.0.0.0:55678
|
||||
otlp/spanmetrics:
|
||||
@ -85,6 +110,17 @@ exporters:
|
||||
endpoint: 0.0.0.0:8889
|
||||
# logging: {}
|
||||
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/
|
||||
timeout: 5s
|
||||
sending_queue:
|
||||
queue_size: 100
|
||||
retry_on_failure:
|
||||
enabled: true
|
||||
initial_interval: 5s
|
||||
max_interval: 30s
|
||||
max_elapsed_time: 300s
|
||||
|
||||
service:
|
||||
telemetry:
|
||||
metrics:
|
||||
@ -109,3 +145,7 @@ service:
|
||||
metrics/spanmetrics:
|
||||
receivers: [otlp/spanmetrics]
|
||||
exporters: [prometheus]
|
||||
logs:
|
||||
receivers: [otlp, filelog/dockercontainers]
|
||||
processors: [batch]
|
||||
exporters: [clickhouselogsexporter]
|
@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
build
|
||||
*.typegen.ts
|
||||
i18-generate-hash.js
|
24
frontend/i18-generate-hash.js
Normal file
24
frontend/i18-generate-hash.js
Normal file
@ -0,0 +1,24 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
// @ts-nocheck
|
||||
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
const glob = require('glob');
|
||||
|
||||
function generateChecksum(str, algorithm, encoding) {
|
||||
return crypto
|
||||
.createHash(algorithm || 'md5')
|
||||
.update(str, 'utf8')
|
||||
.digest(encoding || 'hex');
|
||||
}
|
||||
|
||||
const result = {};
|
||||
|
||||
glob.sync(`public/locales/**/*.json`).forEach(path => {
|
||||
const [_, lang] = path.split('public/locales');
|
||||
const content = fs.readFileSync(path, { encoding: 'utf-8' });
|
||||
result[lang.replace('.json', '')] = generateChecksum(content);
|
||||
});
|
||||
|
||||
fs.writeFileSync('./i18n-translations-hash.json', JSON.stringify(result));
|
@ -4,19 +4,20 @@
|
||||
"description": "",
|
||||
"main": "webpack.config.js",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development webpack serve --progress",
|
||||
"build": "webpack --config=webpack.config.prod.js --progress",
|
||||
"i18n:generate-hash": "node ./i18-generate-hash.js",
|
||||
"dev": "npm run i18n:generate-hash && cross-env NODE_ENV=development webpack serve --progress",
|
||||
"build": "npm run i18n:generate-hash && webpack --config=webpack.config.prod.js --progress",
|
||||
"prettify": "prettier --write .",
|
||||
"lint": "eslint ./src",
|
||||
"lint:fix": "eslint ./src --fix",
|
||||
"lint": "npm run i18n:generate-hash && eslint ./src",
|
||||
"lint:fix": "npm run i18n:generate-hash && eslint ./src --fix",
|
||||
"jest": "jest",
|
||||
"jest:coverage": "jest --coverage",
|
||||
"jest:watch": "jest --watch",
|
||||
"postinstall": "is-ci || yarn husky:configure",
|
||||
"playwright": "NODE_ENV=testing playwright test --config=./playwright.config.ts",
|
||||
"playwright": "npm run i18n:generate-hash && NODE_ENV=testing playwright test --config=./playwright.config.ts",
|
||||
"playwright:local:debug": "PWDEBUG=console yarn playwright --headed --browser=chromium",
|
||||
"playwright:codegen:local":"playwright codegen http://localhost:3301",
|
||||
"playwright:codegen:local:auth":"yarn playwright:codegen:local --load-storage=tests/auth.json",
|
||||
"playwright:codegen:local": "playwright codegen http://localhost:3301",
|
||||
"playwright:codegen:local:auth": "yarn playwright:codegen:local --load-storage=tests/auth.json",
|
||||
"husky:configure": "cd .. && husky install frontend/.husky && cd frontend && chmod ug+x .husky/*",
|
||||
"commitlint": "commitlint --edit $1"
|
||||
},
|
||||
@ -55,7 +56,9 @@
|
||||
"d3-tip": "^0.9.1",
|
||||
"dayjs": "^1.10.7",
|
||||
"dotenv": "8.2.0",
|
||||
"event-source-polyfill": "1.0.31",
|
||||
"file-loader": "6.1.1",
|
||||
"flat": "^5.0.2",
|
||||
"history": "4.10.1",
|
||||
"html-webpack-plugin": "5.1.0",
|
||||
"i18next": "^21.6.12",
|
||||
@ -123,6 +126,8 @@
|
||||
"@types/copy-webpack-plugin": "^8.0.1",
|
||||
"@types/d3": "^6.2.0",
|
||||
"@types/d3-tip": "^3.5.5",
|
||||
"@types/event-source-polyfill": "^1.0.0",
|
||||
"@types/flat": "^5.0.2",
|
||||
"@types/jest": "^27.5.1",
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"@types/mini-css-extract-plugin": "^2.5.1",
|
||||
|
@ -100,6 +100,10 @@ export const MySettings = Loadable(
|
||||
() => import(/* webpackChunkName: "All MySettings" */ 'pages/MySettings'),
|
||||
);
|
||||
|
||||
export const Logs = Loadable(
|
||||
() => import(/* webpackChunkName: "Logs" */ 'pages/Logs'),
|
||||
);
|
||||
|
||||
export const Login = Loadable(
|
||||
() => import(/* webpackChunkName: "Login" */ 'pages/Login'),
|
||||
);
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
InstrumentationPage,
|
||||
ListAllALertsPage,
|
||||
Login,
|
||||
Logs,
|
||||
MySettings,
|
||||
NewDashboardPage,
|
||||
OrganizationSettings,
|
||||
@ -193,6 +194,13 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'MY_SETTINGS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.LOGS,
|
||||
exact: true,
|
||||
component: Logs,
|
||||
key: 'LOGS',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.LOGIN,
|
||||
exact: true,
|
||||
|
@ -3,6 +3,8 @@ import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import cacheBursting from '../../i18n-translations-hash.json';
|
||||
|
||||
i18n
|
||||
// load translation using http -> see /public/locales
|
||||
.use(Backend)
|
||||
@ -17,7 +19,14 @@ i18n
|
||||
interpolation: {
|
||||
escapeValue: false, // not needed for react as it escapes by default
|
||||
},
|
||||
|
||||
backend: {
|
||||
loadPath: (language, namespace) => {
|
||||
const ns = namespace[0];
|
||||
const pathkey = `/${language}/${ns}`;
|
||||
const hash = cacheBursting[pathkey as keyof typeof cacheBursting] || '';
|
||||
return `/locales/${language}/${namespace}.json?h=${hash}`;
|
||||
},
|
||||
},
|
||||
react: {
|
||||
useSuspense: false,
|
||||
},
|
||||
|
23
frontend/src/api/logs/AddToSelectedField.ts
Normal file
23
frontend/src/api/logs/AddToSelectedField.ts
Normal file
@ -0,0 +1,23 @@
|
||||
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/logs/addToSelectedFields';
|
||||
|
||||
const AddToSelectedFields = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const data = await axios.post(`/logs/fields`, props);
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default AddToSelectedFields;
|
26
frontend/src/api/logs/GetLogs.ts
Normal file
26
frontend/src/api/logs/GetLogs.ts
Normal file
@ -0,0 +1,26 @@
|
||||
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/logs/getLogs';
|
||||
|
||||
const GetLogs = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const data = await axios.get(`/logs`, {
|
||||
params: props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data.results,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default GetLogs;
|
26
frontend/src/api/logs/GetLogsAggregate.ts
Normal file
26
frontend/src/api/logs/GetLogsAggregate.ts
Normal file
@ -0,0 +1,26 @@
|
||||
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/logs/getLogsAggregate';
|
||||
|
||||
const GetLogsAggregate = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const data = await axios.get(`/logs/aggregate`, {
|
||||
params: props,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data.items,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default GetLogsAggregate;
|
24
frontend/src/api/logs/GetSearchFields.ts
Normal file
24
frontend/src/api/logs/GetSearchFields.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
import { PayloadProps } from 'types/api/logs/getSearchFields';
|
||||
|
||||
const GetSearchFields = async (): Promise<
|
||||
SuccessResponse<PayloadProps> | ErrorResponse
|
||||
> => {
|
||||
try {
|
||||
const data = await axios.get(`/logs/fields`);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default GetSearchFields;
|
23
frontend/src/api/logs/RemoveFromSelectedField.ts
Normal file
23
frontend/src/api/logs/RemoveFromSelectedField.ts
Normal file
@ -0,0 +1,23 @@
|
||||
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/logs/addToSelectedFields';
|
||||
|
||||
const RemoveSelectedField = async (
|
||||
props: Props,
|
||||
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
|
||||
try {
|
||||
const data = await axios.post(`/logs/fields`, props);
|
||||
return {
|
||||
statusCode: 200,
|
||||
error: null,
|
||||
message: '',
|
||||
payload: data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
return ErrorResponseHandler(error as AxiosError);
|
||||
}
|
||||
};
|
||||
|
||||
export default RemoveSelectedField;
|
17
frontend/src/api/logs/livetail.ts
Normal file
17
frontend/src/api/logs/livetail.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import apiV1 from 'api/apiV1';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
|
||||
export const LiveTail = (queryParams: string): EventSourcePolyfill => {
|
||||
const dict = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,
|
||||
},
|
||||
};
|
||||
return new EventSourcePolyfill(
|
||||
`${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`,
|
||||
dict,
|
||||
);
|
||||
};
|
@ -65,6 +65,7 @@ function Graph({
|
||||
yAxisUnit = 'short',
|
||||
forceReRender,
|
||||
staticLine,
|
||||
containerHeight,
|
||||
}: GraphProps): JSX.Element {
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const chartRef = useRef<HTMLCanvasElement>(null);
|
||||
@ -241,7 +242,7 @@ function Graph({
|
||||
}, [buildChart, forceReRender]);
|
||||
|
||||
return (
|
||||
<div style={{ height: '85%' }}>
|
||||
<div style={{ height: containerHeight }}>
|
||||
<canvas ref={chartRef} />
|
||||
<LegendsContainer id={name} />
|
||||
</div>
|
||||
@ -259,6 +260,7 @@ interface GraphProps {
|
||||
yAxisUnit?: string;
|
||||
forceReRender?: boolean | null | number;
|
||||
staticLine?: StaticLineProps | undefined;
|
||||
containerHeight?: string | number;
|
||||
}
|
||||
|
||||
export interface StaticLineProps {
|
||||
@ -285,5 +287,6 @@ Graph.defaultProps = {
|
||||
yAxisUnit: undefined,
|
||||
forceReRender: undefined,
|
||||
staticLine: undefined,
|
||||
containerHeight: '85%',
|
||||
};
|
||||
export default Graph;
|
||||
|
147
frontend/src/components/Logs/AddToQueryHOC.tsx
Normal file
147
frontend/src/components/Logs/AddToQueryHOC.tsx
Normal file
@ -0,0 +1,147 @@
|
||||
import { Button, Popover } from 'antd';
|
||||
import getStep from 'lib/getStep';
|
||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||
import React, { memo, useCallback, 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';
|
||||
|
||||
interface AddToQueryHOCProps {
|
||||
fieldKey: string;
|
||||
fieldValue: string;
|
||||
children: React.ReactNode;
|
||||
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||
getLogsAggregate: (
|
||||
props: Parameters<typeof getLogsAggregate>[0],
|
||||
) => ReturnType<typeof getLogsAggregate>;
|
||||
}
|
||||
function AddToQueryHOC({
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
children,
|
||||
getLogs,
|
||||
getLogsAggregate,
|
||||
}: AddToQueryHOCProps): JSX.Element {
|
||||
const {
|
||||
searchFilter: { queryString },
|
||||
logLinesPerPage,
|
||||
idStart,
|
||||
idEnd,
|
||||
liveTail,
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const generatedQuery = useMemo(
|
||||
() => generateFilterQuery({ fieldKey, fieldValue, type: 'IN' }),
|
||||
[fieldKey, fieldValue],
|
||||
);
|
||||
|
||||
const handleQueryAdd = useCallback(() => {
|
||||
let updatedQueryString = queryString || '';
|
||||
|
||||
if (updatedQueryString.length === 0) {
|
||||
updatedQueryString += `${generatedQuery}`;
|
||||
} else {
|
||||
updatedQueryString += ` AND ${generatedQuery}`;
|
||||
}
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_STRING,
|
||||
payload: updatedQueryString,
|
||||
});
|
||||
if (liveTail === 'STOPPED') {
|
||||
getLogs({
|
||||
q: updatedQueryString,
|
||||
limit: logLinesPerPage,
|
||||
orderBy: 'timestamp',
|
||||
order: 'desc',
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
getLogsAggregate({
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
step: getStep({
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: updatedQueryString,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
} else if (liveTail === 'PLAYING') {
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: 'PAUSED',
|
||||
});
|
||||
setTimeout(
|
||||
() =>
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: liveTail,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
dispatch,
|
||||
generatedQuery,
|
||||
getLogs,
|
||||
idEnd,
|
||||
idStart,
|
||||
logLinesPerPage,
|
||||
maxTime,
|
||||
minTime,
|
||||
queryString,
|
||||
]);
|
||||
|
||||
const popOverContent = (
|
||||
<span style={{ fontSize: '0.9rem' }}>Add to query: {fieldKey}</span>
|
||||
);
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
}}
|
||||
onClick={handleQueryAdd}
|
||||
>
|
||||
<Popover placement="top" content={popOverContent}>
|
||||
{children}
|
||||
</Popover>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogs: (
|
||||
props: Parameters<typeof getLogs>[0],
|
||||
) => (dispatch: Dispatch<AppActions>) => void;
|
||||
getLogsAggregate: (
|
||||
props: Parameters<typeof getLogsAggregate>[0],
|
||||
) => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogs: bindActionCreators(getLogs, dispatch),
|
||||
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(AddToQueryHOC));
|
12
frontend/src/components/Logs/CategoryHeading/index.tsx
Normal file
12
frontend/src/components/Logs/CategoryHeading/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import { CategoryHeadingText } from './styles';
|
||||
|
||||
interface ICategoryHeadingProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
function CategoryHeading({ children }: ICategoryHeadingProps): JSX.Element {
|
||||
return <CategoryHeadingText type="secondary">{children}</CategoryHeadingText>;
|
||||
}
|
||||
|
||||
export default CategoryHeading;
|
6
frontend/src/components/Logs/CategoryHeading/styles.ts
Normal file
6
frontend/src/components/Logs/CategoryHeading/styles.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CategoryHeadingText = styled(Typography.Text)`
|
||||
font-size: 0.8rem;
|
||||
`;
|
37
frontend/src/components/Logs/CopyClipboardHOC.tsx
Normal file
37
frontend/src/components/Logs/CopyClipboardHOC.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Popover } from 'antd';
|
||||
import React from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
|
||||
interface CopyClipboardHOCProps {
|
||||
textToCopy: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
function CopyClipboardHOC({
|
||||
textToCopy,
|
||||
children,
|
||||
}: CopyClipboardHOCProps): JSX.Element {
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
|
||||
return (
|
||||
<span
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={(): void => setCopy(textToCopy)}
|
||||
onKeyDown={(): void => setCopy(textToCopy)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<Popover
|
||||
placement="top"
|
||||
content={<span style={{ fontSize: '0.9rem' }}>Copy to clipboard</span>}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default CopyClipboardHOC;
|
157
frontend/src/components/Logs/LogItem/index.tsx
Normal file
157
frontend/src/components/Logs/LogItem/index.tsx
Normal file
@ -0,0 +1,157 @@
|
||||
import { blue, grey, orange } from '@ant-design/colors';
|
||||
import { CopyFilled, ExpandAltOutlined } from '@ant-design/icons';
|
||||
import { Button, Divider, Row, Typography } from 'antd';
|
||||
import { map } from 'd3';
|
||||
import dayjs from 'dayjs';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import AddToQueryHOC from '../AddToQueryHOC';
|
||||
import CopyClipboardHOC from '../CopyClipboardHOC';
|
||||
import { Container } from './styles';
|
||||
import { isValidLogField } from './util';
|
||||
|
||||
interface LogFieldProps {
|
||||
fieldKey: string;
|
||||
fieldValue: string;
|
||||
}
|
||||
function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Typography.Text type="secondary">{fieldKey}</Typography.Text>
|
||||
<CopyClipboardHOC textToCopy={fieldValue}>
|
||||
<Typography.Text ellipsis>
|
||||
{': '}
|
||||
{fieldValue}
|
||||
</Typography.Text>
|
||||
</CopyClipboardHOC>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function LogSelectedField({
|
||||
fieldKey = '',
|
||||
fieldValue = '',
|
||||
}: LogFieldProps): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
overflow: 'hidden',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<AddToQueryHOC fieldKey={fieldKey} fieldValue={fieldValue}>
|
||||
<Typography.Text>
|
||||
{`"`}
|
||||
<span style={{ color: blue[4] }}>{fieldKey}</span>
|
||||
{`"`}
|
||||
</Typography.Text>
|
||||
</AddToQueryHOC>
|
||||
<CopyClipboardHOC textToCopy={fieldValue}>
|
||||
<Typography.Text ellipsis>
|
||||
<span>
|
||||
{': '}
|
||||
{typeof fieldValue === 'string' && `"`}
|
||||
</span>
|
||||
<span style={{ color: orange[6] }}>{fieldValue}</span>
|
||||
{typeof fieldValue === 'string' && `"`}
|
||||
</Typography.Text>
|
||||
</CopyClipboardHOC>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface LogItemProps {
|
||||
logData: ILog;
|
||||
}
|
||||
function LogItem({ logData }: LogItemProps): JSX.Element {
|
||||
const {
|
||||
fields: { selected },
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
const dispatch = useDispatch();
|
||||
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
|
||||
const handleDetailedView = useCallback(() => {
|
||||
dispatch({
|
||||
type: SET_DETAILED_LOG_DATA,
|
||||
payload: logData,
|
||||
});
|
||||
}, [dispatch, logData]);
|
||||
|
||||
const handleCopyJSON = (): void => {
|
||||
setCopy(JSON.stringify(logData, null, 2));
|
||||
};
|
||||
return (
|
||||
<Container>
|
||||
<div style={{ maxWidth: '100%' }}>
|
||||
<div>
|
||||
{'{'}
|
||||
<div style={{ marginLeft: '0.5rem' }}>
|
||||
<LogGeneralField
|
||||
fieldKey="log"
|
||||
fieldValue={flattenLogData.body as never}
|
||||
/>
|
||||
{flattenLogData.stream && (
|
||||
<LogGeneralField
|
||||
fieldKey="stream"
|
||||
fieldValue={flattenLogData.stream as never}
|
||||
/>
|
||||
)}
|
||||
<LogGeneralField
|
||||
fieldKey="timestamp"
|
||||
fieldValue={dayjs((flattenLogData.timestamp as never) / 1e6).format()}
|
||||
/>
|
||||
</div>
|
||||
{'}'}
|
||||
</div>
|
||||
<div>
|
||||
{map(selected, (field) => {
|
||||
return isValidLogField(flattenLogData[field.name] as never) ? (
|
||||
<LogSelectedField
|
||||
key={field.name}
|
||||
fieldKey={field.name}
|
||||
fieldValue={flattenLogData[field.name] as never}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Divider style={{ padding: 0, margin: '0.4rem 0', opacity: 0.5 }} />
|
||||
<Row>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={handleDetailedView}
|
||||
style={{ color: blue[5], padding: 0, margin: 0 }}
|
||||
>
|
||||
{' '}
|
||||
<ExpandAltOutlined /> View Details
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={handleCopyJSON}
|
||||
style={{ padding: 0, margin: 0, color: grey[1] }}
|
||||
>
|
||||
{' '}
|
||||
<CopyFilled /> Copy JSON
|
||||
</Button>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogItem;
|
18
frontend/src/components/Logs/LogItem/styles.ts
Normal file
18
frontend/src/components/Logs/LogItem/styles.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Card } from 'antd';
|
||||
import styled, { keyframes } from 'styled-components';
|
||||
|
||||
const fadeInAnimation = keyframes`
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1;}
|
||||
`;
|
||||
|
||||
export const Container = styled(Card)`
|
||||
width: 100% !important;
|
||||
margin-bottom: 0.3rem;
|
||||
.ant-card-body {
|
||||
padding: 0.3rem 0.6rem;
|
||||
}
|
||||
animation-name: ${fadeInAnimation};
|
||||
animation-duration: 0.2s;
|
||||
animation-timing-function: ease-in;
|
||||
`;
|
1
frontend/src/components/Logs/LogItem/util.ts
Normal file
1
frontend/src/components/Logs/LogItem/util.ts
Normal file
@ -0,0 +1 @@
|
||||
export const isValidLogField = (value: never): boolean => value !== undefined;
|
@ -30,4 +30,5 @@ export const QueryBuilderQueryTemplate = {
|
||||
export const QueryBuilderFormulaTemplate = {
|
||||
expression: '',
|
||||
disabled: false,
|
||||
legend: '',
|
||||
};
|
||||
|
@ -26,6 +26,7 @@ const ROUTES = {
|
||||
SOMETHING_WENT_WRONG: '/something-went-wrong',
|
||||
UN_AUTHORIZED: '/un-authorized',
|
||||
NOT_FOUND: '/not-found',
|
||||
LOGS: '/logs',
|
||||
HOME_PAGE: '/',
|
||||
PASSWORD_RESET: '/password-reset',
|
||||
};
|
||||
|
@ -68,15 +68,20 @@ function QuerySection({
|
||||
const handleFormulaChange = ({
|
||||
formulaIndex,
|
||||
expression,
|
||||
legend,
|
||||
toggleDisable,
|
||||
toggleDelete,
|
||||
}: IQueryBuilderFormulaHandleChange): void => {
|
||||
const allFormulas = formulaQueries;
|
||||
const current = allFormulas[formulaIndex];
|
||||
if (expression) {
|
||||
if (expression !== undefined) {
|
||||
current.expression = expression;
|
||||
}
|
||||
|
||||
if (legend !== undefined) {
|
||||
current.legend = legend;
|
||||
}
|
||||
|
||||
if (toggleDisable) {
|
||||
current.disabled = !current.disabled;
|
||||
}
|
||||
@ -179,6 +184,7 @@ function QuerySection({
|
||||
formulaOnly: true,
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
legend: '',
|
||||
};
|
||||
|
||||
setFormulaQueries({ ...formulas });
|
||||
|
@ -1,6 +1,8 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Divider,
|
||||
Modal,
|
||||
@ -26,6 +28,7 @@ import {
|
||||
} from 'types/api/disks/getDisks';
|
||||
import { TTTLType } from 'types/api/settings/common';
|
||||
import {
|
||||
PayloadPropsLogs as GetRetentionPeriodLogsPayload,
|
||||
PayloadPropsMetrics as GetRetentionPeriodMetricsPayload,
|
||||
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
|
||||
} from 'types/api/settings/getRetention';
|
||||
@ -40,19 +43,25 @@ type NumberOrNull = number | null;
|
||||
function GeneralSettings({
|
||||
metricsTtlValuesPayload,
|
||||
tracesTtlValuesPayload,
|
||||
logsTtlValuesPayload,
|
||||
getAvailableDiskPayload,
|
||||
metricsTtlValuesRefetch,
|
||||
tracesTtlValuesRefetch,
|
||||
logsTtlValuesRefetch,
|
||||
}: GeneralSettingsProps): JSX.Element {
|
||||
const { t } = useTranslation(['generalSettings']);
|
||||
const [modalMetrics, setModalMetrics] = useState<boolean>(false);
|
||||
const [modalTraces, setModalTraces] = useState<boolean>(false);
|
||||
const [modalLogs, setModalLogs] = useState<boolean>(false);
|
||||
|
||||
const [postApiLoadingMetrics, setPostApiLoadingMetrics] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const [postApiLoadingTraces, setPostApiLoadingTraces] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const [modalTraces, setModalTraces] = useState<boolean>(false);
|
||||
const [postApiLoadingLogs, setPostApiLoadingLogs] = useState<boolean>(false);
|
||||
|
||||
const [availableDisks] = useState<IDiskType[]>(getAvailableDiskPayload);
|
||||
|
||||
const [metricsCurrentTTLValues, setMetricsCurrentTTLValues] = useState(
|
||||
@ -62,6 +71,10 @@ function GeneralSettings({
|
||||
tracesTtlValuesPayload,
|
||||
);
|
||||
|
||||
const [logsCurrentTTLValues, setLogsCurrentTTLValues] = useState(
|
||||
logsTtlValuesPayload,
|
||||
);
|
||||
|
||||
const { role } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
|
||||
const [setRetentionPermission] = useComponentPermission(
|
||||
@ -86,6 +99,15 @@ function GeneralSettings({
|
||||
setTracesS3RetentionPeriod,
|
||||
] = useState<NumberOrNull>(null);
|
||||
|
||||
const [
|
||||
logsTotalRetentionPeriod,
|
||||
setLogsTotalRetentionPeriod,
|
||||
] = useState<NumberOrNull>(null);
|
||||
const [
|
||||
logsS3RetentionPeriod,
|
||||
setLogsS3RetentionPeriod,
|
||||
] = useState<NumberOrNull>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (metricsCurrentTTLValues) {
|
||||
setMetricsTotalRetentionPeriod(
|
||||
@ -112,6 +134,17 @@ function GeneralSettings({
|
||||
}
|
||||
}, [tracesCurrentTTLValues]);
|
||||
|
||||
useEffect(() => {
|
||||
if (logsCurrentTTLValues) {
|
||||
setLogsTotalRetentionPeriod(logsCurrentTTLValues.logs_ttl_duration_hrs);
|
||||
setLogsS3RetentionPeriod(
|
||||
logsCurrentTTLValues.logs_move_ttl_duration_hrs
|
||||
? logsCurrentTTLValues.logs_move_ttl_duration_hrs
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}, [logsCurrentTTLValues]);
|
||||
|
||||
useInterval(
|
||||
async (): Promise<void> => {
|
||||
if (metricsTtlValuesPayload.status === 'pending') {
|
||||
@ -130,13 +163,24 @@ function GeneralSettings({
|
||||
tracesTtlValuesPayload.status === 'pending' ? 1000 : null,
|
||||
);
|
||||
|
||||
useInterval(
|
||||
async (): Promise<void> => {
|
||||
if (logsTtlValuesPayload.status === 'pending') {
|
||||
logsTtlValuesRefetch();
|
||||
}
|
||||
},
|
||||
logsTtlValuesPayload.status === 'pending' ? 1000 : null,
|
||||
);
|
||||
|
||||
const onModalToggleHandler = (type: TTTLType): void => {
|
||||
if (type === 'metrics') setModalMetrics((modal) => !modal);
|
||||
if (type === 'traces') setModalTraces((modal) => !modal);
|
||||
if (type === 'logs') setModalLogs((modal) => !modal);
|
||||
};
|
||||
const onPostApiLoadingHandler = (type: TTTLType): void => {
|
||||
if (type === 'metrics') setPostApiLoadingMetrics((modal) => !modal);
|
||||
if (type === 'traces') setPostApiLoadingTraces((modal) => !modal);
|
||||
if (type === 'logs') setPostApiLoadingLogs((modal) => !modal);
|
||||
};
|
||||
|
||||
const onClickSaveHandler = useCallback(
|
||||
@ -157,7 +201,13 @@ function GeneralSettings({
|
||||
[availableDisks],
|
||||
);
|
||||
|
||||
const [isMetricsSaveDisabled, isTracesSaveDisabled, errorText] = useMemo((): [
|
||||
const [
|
||||
isMetricsSaveDisabled,
|
||||
isTracesSaveDisabled,
|
||||
isLogsSaveDisabled,
|
||||
errorText,
|
||||
] = useMemo((): [
|
||||
boolean,
|
||||
boolean,
|
||||
boolean,
|
||||
string,
|
||||
@ -174,6 +224,7 @@ function GeneralSettings({
|
||||
// Defaults to button not disabled and empty error message text.
|
||||
let isMetricsSaveDisabled = false;
|
||||
let isTracesSaveDisabled = false;
|
||||
let isLogsSaveDisabled = false;
|
||||
let errorText = '';
|
||||
|
||||
if (s3Enabled) {
|
||||
@ -189,18 +240,35 @@ function GeneralSettings({
|
||||
) {
|
||||
isTracesSaveDisabled = true;
|
||||
errorText = messages.compareError('traces');
|
||||
} else if (
|
||||
(logsTotalRetentionPeriod || logsS3RetentionPeriod) &&
|
||||
Number(logsTotalRetentionPeriod) <= Number(logsS3RetentionPeriod)
|
||||
) {
|
||||
isLogsSaveDisabled = true;
|
||||
errorText = messages.compareError('logs');
|
||||
}
|
||||
}
|
||||
|
||||
if (!metricsTotalRetentionPeriod || !tracesTotalRetentionPeriod) {
|
||||
if (
|
||||
!metricsTotalRetentionPeriod ||
|
||||
!tracesTotalRetentionPeriod ||
|
||||
!logsTotalRetentionPeriod
|
||||
) {
|
||||
isMetricsSaveDisabled = true;
|
||||
isTracesSaveDisabled = true;
|
||||
if (!metricsTotalRetentionPeriod && !tracesTotalRetentionPeriod) {
|
||||
errorText = messages.nullValueError('metrics and traces');
|
||||
isLogsSaveDisabled = true;
|
||||
if (
|
||||
!metricsTotalRetentionPeriod &&
|
||||
!tracesTotalRetentionPeriod &&
|
||||
!logsTotalRetentionPeriod
|
||||
) {
|
||||
errorText = messages.nullValueError('metrics, traces and logs');
|
||||
} else if (!metricsTotalRetentionPeriod) {
|
||||
errorText = messages.nullValueError('metrics');
|
||||
} else if (!tracesTotalRetentionPeriod) {
|
||||
errorText = messages.nullValueError('traces');
|
||||
} else if (!logsTotalRetentionPeriod) {
|
||||
errorText = messages.nullValueError('logs');
|
||||
}
|
||||
}
|
||||
if (
|
||||
@ -219,8 +287,23 @@ function GeneralSettings({
|
||||
)
|
||||
isTracesSaveDisabled = true;
|
||||
|
||||
return [isMetricsSaveDisabled, isTracesSaveDisabled, errorText];
|
||||
if (
|
||||
logsCurrentTTLValues.logs_ttl_duration_hrs === logsTotalRetentionPeriod &&
|
||||
logsCurrentTTLValues.logs_move_ttl_duration_hrs === logsS3RetentionPeriod
|
||||
)
|
||||
isLogsSaveDisabled = true;
|
||||
|
||||
return [
|
||||
isMetricsSaveDisabled,
|
||||
isTracesSaveDisabled,
|
||||
isLogsSaveDisabled,
|
||||
errorText,
|
||||
];
|
||||
}, [
|
||||
logsCurrentTTLValues.logs_move_ttl_duration_hrs,
|
||||
logsCurrentTTLValues.logs_ttl_duration_hrs,
|
||||
logsS3RetentionPeriod,
|
||||
logsTotalRetentionPeriod,
|
||||
metricsCurrentTTLValues.metrics_move_ttl_duration_hrs,
|
||||
metricsCurrentTTLValues?.metrics_ttl_duration_hrs,
|
||||
metricsS3RetentionPeriod,
|
||||
@ -235,21 +318,36 @@ function GeneralSettings({
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const onOkHandler = async (type: TTTLType): Promise<void> => {
|
||||
let apiCallTotalRetention;
|
||||
let apiCallS3Retention;
|
||||
|
||||
switch (type) {
|
||||
case 'metrics': {
|
||||
apiCallTotalRetention = metricsTotalRetentionPeriod;
|
||||
apiCallS3Retention = metricsS3RetentionPeriod;
|
||||
break;
|
||||
}
|
||||
case 'traces': {
|
||||
apiCallTotalRetention = tracesTotalRetentionPeriod;
|
||||
apiCallS3Retention = tracesS3RetentionPeriod;
|
||||
break;
|
||||
}
|
||||
case 'logs': {
|
||||
apiCallTotalRetention = logsTotalRetentionPeriod;
|
||||
apiCallS3Retention = logsS3RetentionPeriod;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
try {
|
||||
onPostApiLoadingHandler(type);
|
||||
const setTTLResponse = await setRetentionApi({
|
||||
type,
|
||||
totalDuration: `${
|
||||
(type === 'metrics'
|
||||
? metricsTotalRetentionPeriod
|
||||
: tracesTotalRetentionPeriod) || -1
|
||||
}h`,
|
||||
totalDuration: `${apiCallTotalRetention || -1}h`,
|
||||
coldStorage: s3Enabled ? 's3' : null,
|
||||
toColdDuration: `${
|
||||
(type === 'metrics'
|
||||
? metricsS3RetentionPeriod
|
||||
: tracesS3RetentionPeriod) || -1
|
||||
}h`,
|
||||
toColdDuration: `${apiCallS3Retention || -1}h`,
|
||||
});
|
||||
let hasSetTTLFailed = false;
|
||||
if (setTTLResponse.statusCode === 409) {
|
||||
@ -281,6 +379,15 @@ function GeneralSettings({
|
||||
traces_move_ttl_duration_hrs: tracesS3RetentionPeriod || -1,
|
||||
status: '',
|
||||
});
|
||||
} else if (type === 'logs') {
|
||||
logsTtlValuesRefetch();
|
||||
if (!hasSetTTLFailed)
|
||||
// Updates the currentTTL Values in order to avoid pushing the same values.
|
||||
setLogsCurrentTTLValues({
|
||||
logs_ttl_duration_hrs: logsTotalRetentionPeriod || -1,
|
||||
logs_move_ttl_duration_hrs: logsS3RetentionPeriod || -1,
|
||||
status: '',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
notification.error({
|
||||
@ -375,63 +482,107 @@ function GeneralSettings({
|
||||
/>
|
||||
),
|
||||
},
|
||||
].map((category, idx, renderArr): JSX.Element | null => {
|
||||
{
|
||||
name: 'Logs',
|
||||
retentionFields: [
|
||||
{
|
||||
name: t('total_retention_period'),
|
||||
value: logsTotalRetentionPeriod,
|
||||
setValue: setLogsTotalRetentionPeriod,
|
||||
},
|
||||
{
|
||||
name: t('move_to_s3'),
|
||||
value: logsS3RetentionPeriod,
|
||||
setValue: setLogsS3RetentionPeriod,
|
||||
hide: !s3Enabled,
|
||||
},
|
||||
],
|
||||
save: {
|
||||
modal: modalLogs,
|
||||
modalOpen: (): void => onClickSaveHandler('logs'),
|
||||
apiLoading: postApiLoadingLogs,
|
||||
saveButtonText:
|
||||
logsTtlValuesPayload.status === 'pending' ? (
|
||||
<span>
|
||||
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />{' '}
|
||||
{t('retention_save_button.pending', { name: 'logs' })}
|
||||
</span>
|
||||
) : (
|
||||
<span>{t('retention_save_button.success')}</span>
|
||||
),
|
||||
isDisabled: logsTtlValuesPayload.status === 'pending' || isLogsSaveDisabled,
|
||||
},
|
||||
statusComponent: (
|
||||
<StatusMessage
|
||||
total_retention={logsTtlValuesPayload.expected_logs_ttl_duration_hrs}
|
||||
status={logsTtlValuesPayload.status}
|
||||
s3_retention={logsTtlValuesPayload.expected_logs_move_ttl_duration_hrs}
|
||||
/>
|
||||
),
|
||||
},
|
||||
].map((category): JSX.Element | null => {
|
||||
if (
|
||||
Array.isArray(category.retentionFields) &&
|
||||
category.retentionFields.length > 0
|
||||
) {
|
||||
return (
|
||||
<React.Fragment key={category.name}>
|
||||
<Col xs={22} xl={11} key={category.name}>
|
||||
<Typography.Title level={3}>{category.name}</Typography.Title>
|
||||
|
||||
{category.retentionFields.map((retentionField) => (
|
||||
<Retention
|
||||
key={retentionField.name}
|
||||
text={retentionField.name}
|
||||
retentionValue={retentionField.value}
|
||||
setRetentionValue={retentionField.setValue}
|
||||
hide={!!retentionField.hide}
|
||||
<Col xs={22} xl={11} key={category.name} style={{ margin: '0.5rem' }}>
|
||||
<Card style={{ height: '100%', minHeight: 300 }}>
|
||||
<Typography.Title style={{ margin: 0 }} level={3}>
|
||||
{category.name}
|
||||
</Typography.Title>
|
||||
<Divider
|
||||
style={{
|
||||
margin: '0.5rem 0',
|
||||
padding: 0,
|
||||
opacity: 0.5,
|
||||
marginBottom: '1rem',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<ActionItemsContainer>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={category.save.modalOpen}
|
||||
disabled={category.save.isDisabled}
|
||||
{category.retentionFields.map((retentionField) => (
|
||||
<Retention
|
||||
key={retentionField.name}
|
||||
text={retentionField.name}
|
||||
retentionValue={retentionField.value}
|
||||
setRetentionValue={retentionField.setValue}
|
||||
hide={!!retentionField.hide}
|
||||
/>
|
||||
))}
|
||||
<ActionItemsContainer>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={category.save.modalOpen}
|
||||
disabled={category.save.isDisabled}
|
||||
>
|
||||
{category.save.saveButtonText}
|
||||
</Button>
|
||||
{category.statusComponent}
|
||||
</ActionItemsContainer>
|
||||
<Modal
|
||||
title={t('retention_confirmation')}
|
||||
focusTriggerAfterClose
|
||||
forceRender
|
||||
destroyOnClose
|
||||
closable
|
||||
onCancel={(): void =>
|
||||
onModalToggleHandler(category.name.toLowerCase() as TTTLType)
|
||||
}
|
||||
onOk={(): Promise<void> =>
|
||||
onOkHandler(category.name.toLowerCase() as TTTLType)
|
||||
}
|
||||
centered
|
||||
visible={category.save.modal}
|
||||
confirmLoading={category.save.apiLoading}
|
||||
>
|
||||
{category.save.saveButtonText}
|
||||
</Button>
|
||||
{category.statusComponent}
|
||||
</ActionItemsContainer>
|
||||
<Modal
|
||||
title={t('retention_confirmation')}
|
||||
focusTriggerAfterClose
|
||||
forceRender
|
||||
destroyOnClose
|
||||
closable
|
||||
onCancel={(): void =>
|
||||
onModalToggleHandler(category.name.toLowerCase() as TTTLType)
|
||||
}
|
||||
onOk={(): Promise<void> =>
|
||||
onOkHandler(category.name.toLowerCase() as TTTLType)
|
||||
}
|
||||
centered
|
||||
visible={category.save.modal}
|
||||
confirmLoading={category.save.apiLoading}
|
||||
>
|
||||
<Typography>
|
||||
{t('retention_confirmation_description', {
|
||||
name: category.name.toLowerCase(),
|
||||
})}
|
||||
</Typography>
|
||||
</Modal>
|
||||
<Typography>
|
||||
{t('retention_confirmation_description', {
|
||||
name: category.name.toLowerCase(),
|
||||
})}
|
||||
</Typography>
|
||||
</Modal>
|
||||
</Card>
|
||||
</Col>
|
||||
{idx < renderArr.length && (
|
||||
<Col xs={0} xl={1} style={{ textAlign: 'center' }}>
|
||||
<Divider type="vertical" dashed style={{ height: '100%' }} />
|
||||
</Col>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
@ -451,7 +602,7 @@ function GeneralSettings({
|
||||
{errorText && <ErrorText>{errorText}</ErrorText>}
|
||||
</ErrorTextContainer>
|
||||
|
||||
<Row justify="space-around">{renderConfig}</Row>
|
||||
<Row justify="start">{renderConfig}</Row>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
@ -460,12 +611,16 @@ interface GeneralSettingsProps {
|
||||
getAvailableDiskPayload: GetDisksPayload;
|
||||
metricsTtlValuesPayload: GetRetentionPeriodMetricsPayload;
|
||||
tracesTtlValuesPayload: GetRetentionPeriodTracesPayload;
|
||||
logsTtlValuesPayload: GetRetentionPeriodLogsPayload;
|
||||
metricsTtlValuesRefetch: UseQueryResult<
|
||||
ErrorResponse | SuccessResponse<GetRetentionPeriodMetricsPayload>
|
||||
>['refetch'];
|
||||
tracesTtlValuesRefetch: UseQueryResult<
|
||||
ErrorResponse | SuccessResponse<GetRetentionPeriodTracesPayload>
|
||||
>['refetch'];
|
||||
logsTtlValuesRefetch: UseQueryResult<
|
||||
ErrorResponse | SuccessResponse<GetRetentionPeriodLogsPayload>
|
||||
>['refetch'];
|
||||
}
|
||||
|
||||
export default GeneralSettings;
|
||||
|
@ -84,10 +84,10 @@ function Retention({
|
||||
return (
|
||||
<RetentionContainer>
|
||||
<Row justify="space-between">
|
||||
<Col flex={1} style={{ display: 'flex' }}>
|
||||
<Col span={12} style={{ display: 'flex' }}>
|
||||
<RetentionFieldLabel>{text}</RetentionFieldLabel>
|
||||
</Col>
|
||||
<Col flex="150px">
|
||||
<Row justify="end">
|
||||
<RetentionFieldInputContainer>
|
||||
<Input
|
||||
value={selectedValue && selectedValue >= 0 ? selectedValue : ''}
|
||||
@ -102,7 +102,7 @@ function Retention({
|
||||
{menuItems}
|
||||
</Select>
|
||||
</RetentionFieldInputContainer>
|
||||
</Col>
|
||||
</Row>
|
||||
</Row>
|
||||
</RetentionContainer>
|
||||
);
|
||||
|
@ -21,6 +21,7 @@ function GeneralSettings(): JSX.Element {
|
||||
const [
|
||||
getRetentionPeriodMetricsApiResponse,
|
||||
getRetentionPeriodTracesApiResponse,
|
||||
getRetentionPeriodLogsApiResponse,
|
||||
getDisksResponse,
|
||||
] = useQueries([
|
||||
{
|
||||
@ -33,6 +34,10 @@ function GeneralSettings(): JSX.Element {
|
||||
getRetentionPeriodApi('traces'),
|
||||
queryKey: 'getRetentionPeriodApiTraces',
|
||||
},
|
||||
{
|
||||
queryFn: (): TRetentionAPIReturn<'logs'> => getRetentionPeriodApi('logs'),
|
||||
queryKey: 'getRetentionPeriodApiLogs',
|
||||
},
|
||||
{
|
||||
queryFn: getDisks,
|
||||
queryKey: 'getDisks',
|
||||
@ -60,17 +65,27 @@ function GeneralSettings(): JSX.Element {
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
// Error State - When RetentionPeriodLogsApi or getDiskApi gets errored out.
|
||||
if (getRetentionPeriodLogsApiResponse.isError || getDisksResponse.isError) {
|
||||
return (
|
||||
<Typography>
|
||||
{getRetentionPeriodLogsApiResponse.data?.error ||
|
||||
getDisksResponse.data?.error ||
|
||||
t('something_went_wrong')}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
// Loading State - When Metrics, Traces and Disk API are in progress and the promise has not been resolved/reject.
|
||||
if (
|
||||
getRetentionPeriodMetricsApiResponse.isLoading ||
|
||||
getDisksResponse.isLoading ||
|
||||
!getDisksResponse.data?.payload ||
|
||||
getRetentionPeriodMetricsApiResponse.isLoading ||
|
||||
!getRetentionPeriodMetricsApiResponse.data?.payload ||
|
||||
getRetentionPeriodTracesApiResponse.isLoading ||
|
||||
getDisksResponse.isLoading ||
|
||||
!getDisksResponse.data?.payload ||
|
||||
!getRetentionPeriodTracesApiResponse.data?.payload
|
||||
!getRetentionPeriodTracesApiResponse.data?.payload ||
|
||||
getRetentionPeriodLogsApiResponse.isLoading ||
|
||||
!getRetentionPeriodLogsApiResponse.data?.payload
|
||||
) {
|
||||
return <Spinner tip="Loading.." height="70vh" />;
|
||||
}
|
||||
@ -83,6 +98,8 @@ function GeneralSettings(): JSX.Element {
|
||||
metricsTtlValuesRefetch: getRetentionPeriodMetricsApiResponse.refetch,
|
||||
tracesTtlValuesPayload: getRetentionPeriodTracesApiResponse.data?.payload,
|
||||
tracesTtlValuesRefetch: getRetentionPeriodTracesApiResponse.refetch,
|
||||
logsTtlValuesPayload: getRetentionPeriodLogsApiResponse.data?.payload,
|
||||
logsTtlValuesRefetch: getRetentionPeriodLogsApiResponse.refetch,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
121
frontend/src/container/LogControls/index.tsx
Normal file
121
frontend/src/container/LogControls/index.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import {
|
||||
FastBackwardOutlined,
|
||||
LeftOutlined,
|
||||
RightOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Divider, Select } from 'antd';
|
||||
import React, { memo } 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 { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
GET_NEXT_LOG_LINES,
|
||||
GET_PREVIOUS_LOG_LINES,
|
||||
RESET_ID_START_AND_END,
|
||||
SET_LOG_LINES_PER_PAGE,
|
||||
} from 'types/actions/logs';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { Container } from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const ITEMS_PER_PAGE_OPTIONS = [25, 50, 100, 200];
|
||||
|
||||
interface LogControlsProps {
|
||||
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||
}
|
||||
function LogControls({ getLogs }: LogControlsProps): JSX.Element | null {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const {
|
||||
logLinesPerPage,
|
||||
idStart,
|
||||
idEnd,
|
||||
liveTail,
|
||||
searchFilter: { queryString },
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleLogLinesPerPageChange = (e: number): void => {
|
||||
dispatch({
|
||||
type: SET_LOG_LINES_PER_PAGE,
|
||||
payload: e,
|
||||
});
|
||||
};
|
||||
|
||||
const handleGoToLatest = (): void => {
|
||||
dispatch({
|
||||
type: RESET_ID_START_AND_END,
|
||||
});
|
||||
|
||||
if (liveTail === 'STOPPED')
|
||||
getLogs({
|
||||
q: queryString,
|
||||
limit: logLinesPerPage,
|
||||
orderBy: 'timestamp',
|
||||
order: 'desc',
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
const handleNavigatePrevious = (): void => {
|
||||
dispatch({
|
||||
type: GET_PREVIOUS_LOG_LINES,
|
||||
});
|
||||
};
|
||||
const handleNavigateNext = (): void => {
|
||||
dispatch({
|
||||
type: GET_NEXT_LOG_LINES,
|
||||
});
|
||||
};
|
||||
|
||||
if (liveTail !== 'STOPPED') {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Container>
|
||||
<Button size="small" type="link" onClick={handleGoToLatest}>
|
||||
<FastBackwardOutlined /> Go to latest
|
||||
</Button>
|
||||
<Divider type="vertical" />
|
||||
<Button size="small" type="link" onClick={handleNavigatePrevious}>
|
||||
<LeftOutlined /> Previous
|
||||
</Button>
|
||||
<Button size="small" type="link" onClick={handleNavigateNext}>
|
||||
Next <RightOutlined />
|
||||
</Button>
|
||||
<Select
|
||||
style={{ width: 120 }}
|
||||
value={logLinesPerPage}
|
||||
onChange={handleLogLinesPerPageChange}
|
||||
>
|
||||
{ITEMS_PER_PAGE_OPTIONS.map((count) => {
|
||||
return <Option key={count} value={count}>{`${count} / page`}</Option>;
|
||||
})}
|
||||
</Select>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogs: (
|
||||
props: Parameters<typeof getLogs>[0],
|
||||
) => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogs: bindActionCreators(getLogs, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(LogControls));
|
9
frontend/src/container/LogControls/styles.ts
Normal file
9
frontend/src/container/LogControls/styles.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
`;
|
167
frontend/src/container/LogDetailedView/ActionItem.tsx
Normal file
167
frontend/src/container/LogDetailedView/ActionItem.tsx
Normal file
@ -0,0 +1,167 @@
|
||||
import { MinusCircleOutlined, PlusCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Col, Popover } from 'antd';
|
||||
import getStep from 'lib/getStep';
|
||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||
import React, { memo, useMemo } from 'react';
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { bindActionCreators } 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({
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
getLogs,
|
||||
getLogsAggregate,
|
||||
}: ActionItemProps): JSX.Element | unknown {
|
||||
const {
|
||||
searchFilter: { queryString },
|
||||
logLinesPerPage,
|
||||
idStart,
|
||||
liveTail,
|
||||
idEnd,
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
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: updatedQueryString,
|
||||
});
|
||||
|
||||
if (liveTail === 'STOPPED') {
|
||||
getLogs({
|
||||
q: updatedQueryString,
|
||||
limit: logLinesPerPage,
|
||||
orderBy: 'timestamp',
|
||||
order: 'desc',
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
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 PopOverMenuContent = useMemo(
|
||||
() => (
|
||||
<Col>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={(): void =>
|
||||
handleQueryAdd(
|
||||
generateFilterQuery({
|
||||
fieldKey,
|
||||
fieldValue: validatedFieldValue,
|
||||
type: 'IN',
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
<PlusCircleOutlined /> Filter for value
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={(): void =>
|
||||
handleQueryAdd(
|
||||
generateFilterQuery({
|
||||
fieldKey,
|
||||
fieldValue: validatedFieldValue,
|
||||
type: 'NIN',
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
<MinusCircleOutlined /> Filter out value
|
||||
</Button>
|
||||
</Col>
|
||||
),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[fieldKey, validatedFieldValue],
|
||||
);
|
||||
return (
|
||||
<Popover placement="bottomLeft" content={PopOverMenuContent} trigger="click">
|
||||
<Button type="text" size="small">
|
||||
...
|
||||
</Button>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
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(ActionItem as any));
|
44
frontend/src/container/LogDetailedView/JsonView.tsx
Normal file
44
frontend/src/container/LogDetailedView/JsonView.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import { blue } from '@ant-design/colors';
|
||||
import { CopyFilled } from '@ant-design/icons';
|
||||
import { Button, Row } from 'antd';
|
||||
import Editor from 'components/Editor';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
interface JSONViewProps {
|
||||
logData: ILog;
|
||||
}
|
||||
function JSONView({ logData }: JSONViewProps): JSX.Element {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const LogJsonData = useMemo(() => JSON.stringify(logData, null, 2), [logData]);
|
||||
return (
|
||||
<div>
|
||||
<Row
|
||||
style={{
|
||||
justifyContent: 'flex-end',
|
||||
margin: '0.5rem 0',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
onClick={(): void => copyToClipboard(LogJsonData)}
|
||||
>
|
||||
<CopyFilled /> <span style={{ color: blue[5] }}>Copy to Clipboard</span>
|
||||
</Button>
|
||||
</Row>
|
||||
<div style={{ marginTop: '0.5rem' }}>
|
||||
<Editor
|
||||
value={LogJsonData}
|
||||
language="json"
|
||||
height="70vh"
|
||||
readOnly
|
||||
onChange={(): void => {}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default JSONView;
|
109
frontend/src/container/LogDetailedView/TableView.tsx
Normal file
109
frontend/src/container/LogDetailedView/TableView.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import { blue, orange } from '@ant-design/colors';
|
||||
import { Input, Table } from 'antd';
|
||||
import AddToQueryHOC from 'components/Logs/AddToQueryHOC';
|
||||
import CopyClipboardHOC from 'components/Logs/CopyClipboardHOC';
|
||||
import flatten from 'flat';
|
||||
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import ActionItem from './ActionItem';
|
||||
|
||||
// Fields which should be restricted from adding it to query
|
||||
const RESTRICTED_FIELDS = ['timestamp'];
|
||||
|
||||
interface TableViewProps {
|
||||
logData: ILog;
|
||||
}
|
||||
function TableView({ logData }: TableViewProps): JSX.Element | null {
|
||||
const [fieldSearchInput, setFieldSearchInput] = useState<string>('');
|
||||
|
||||
const flattenLogData: Record<string, never> | null = useMemo(
|
||||
() => (logData ? flatten(logData) : null),
|
||||
[logData],
|
||||
);
|
||||
if (logData === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dataSource =
|
||||
flattenLogData !== null &&
|
||||
Object.keys(flattenLogData)
|
||||
.filter((field) => fieldSearchFilter(field, fieldSearchInput))
|
||||
.map((key) => {
|
||||
return {
|
||||
key,
|
||||
field: key,
|
||||
value: JSON.stringify(flattenLogData[key]),
|
||||
};
|
||||
});
|
||||
|
||||
if (!dataSource) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'Action',
|
||||
width: 75,
|
||||
render: (fieldData: Record<string, string>): JSX.Element | null => {
|
||||
const fieldKey = fieldData.field.split('.').slice(-1);
|
||||
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
|
||||
return <ActionItem fieldKey={fieldKey} fieldValue={fieldData.value} />;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Field',
|
||||
dataIndex: 'field',
|
||||
key: 'field',
|
||||
width: '35%',
|
||||
render: (field: string): JSX.Element => {
|
||||
const fieldKey = field.split('.').slice(-1);
|
||||
const renderedField = <span style={{ color: blue[4] }}>{field}</span>;
|
||||
|
||||
if (!RESTRICTED_FIELDS.includes(fieldKey[0])) {
|
||||
return (
|
||||
<AddToQueryHOC fieldKey={fieldKey[0]} fieldValue={flattenLogData[field]}>
|
||||
{' '}
|
||||
{renderedField}
|
||||
</AddToQueryHOC>
|
||||
);
|
||||
}
|
||||
return renderedField;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Value',
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
ellipsis: false,
|
||||
render: (field: never): JSX.Element => (
|
||||
<CopyClipboardHOC textToCopy={field}>
|
||||
<span style={{ color: orange[6] }}>{field}</span>
|
||||
</CopyClipboardHOC>
|
||||
),
|
||||
width: '60%',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Input
|
||||
placeholder="Search field names"
|
||||
size="large"
|
||||
value={fieldSearchInput}
|
||||
onChange={(e): void => setFieldSearchInput(e.target.value)}
|
||||
/>
|
||||
<Table
|
||||
// scroll={{ x: true }}
|
||||
tableLayout="fixed"
|
||||
dataSource={dataSource}
|
||||
columns={columns as never}
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default TableView;
|
53
frontend/src/container/LogDetailedView/index.tsx
Normal file
53
frontend/src/container/LogDetailedView/index.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { Drawer, Tabs } from 'antd';
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import JSONView from './JsonView';
|
||||
import TableView from './TableView';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
function LogDetailedView(): JSX.Element {
|
||||
const { detailedLog } = useSelector<AppState, ILogsReducer>(
|
||||
(state) => state.logs,
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
const onDrawerClose = (): void => {
|
||||
dispatch({
|
||||
type: SET_DETAILED_LOG_DATA,
|
||||
payload: null,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{}}>
|
||||
<Drawer
|
||||
width="60%"
|
||||
title="Log Details"
|
||||
placement="right"
|
||||
closable
|
||||
mask={false}
|
||||
onClose={onDrawerClose}
|
||||
visible={detailedLog !== null}
|
||||
getContainer={false}
|
||||
style={{ overscrollBehavior: 'contain' }}
|
||||
>
|
||||
{detailedLog && (
|
||||
<Tabs defaultActiveKey="1">
|
||||
<TabPane tab="Table" key="1">
|
||||
<TableView logData={detailedLog} />
|
||||
</TabPane>
|
||||
<TabPane tab="JSON" key="2">
|
||||
<JSONView logData={detailedLog} />
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogDetailedView;
|
26
frontend/src/container/LogLiveTail/OptionIcon.tsx
Normal file
26
frontend/src/container/LogLiveTail/OptionIcon.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
|
||||
interface OptionIconProps {
|
||||
isDarkMode: boolean;
|
||||
}
|
||||
function OptionIcon({ isDarkMode }: OptionIconProps): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="1rem"
|
||||
height="1rem"
|
||||
viewBox="0 0 52 52"
|
||||
enableBackground="new 0 0 52 52"
|
||||
fill={isDarkMode ? '#eee' : '#222'}
|
||||
>
|
||||
<path
|
||||
d="M20,44c0-3.3,2.7-6,6-6s6,2.7,6,6s-2.7,6-6,6S20,47.3,20,44z M20,26c0-3.3,2.7-6,6-6s6,2.7,6,6s-2.7,6-6,6
|
||||
S20,29.3,20,26z M20,8c0-3.3,2.7-6,6-6s6,2.7,6,6s-2.7,6-6,6S20,11.3,20,8z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default OptionIcon;
|
222
frontend/src/container/LogLiveTail/index.tsx
Normal file
222
frontend/src/container/LogLiveTail/index.tsx
Normal file
@ -0,0 +1,222 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { green } from '@ant-design/colors';
|
||||
import { PauseOutlined, PlayCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Popover, Row, Select } from 'antd';
|
||||
import { LiveTail } from 'api/logs/livetail';
|
||||
import dayjs from 'dayjs';
|
||||
import { throttle } from 'lodash-es';
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import {
|
||||
FLUSH_LOGS,
|
||||
PUSH_LIVE_TAIL_EVENT,
|
||||
SET_LIVE_TAIL_START_TIME,
|
||||
TOGGLE_LIVE_TAIL,
|
||||
} from 'types/actions/logs';
|
||||
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import OptionIcon from './OptionIcon';
|
||||
import { TimePickerCard, TimePickerSelect } from './styles';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
const TIME_PICKER_OPTIONS = [
|
||||
{
|
||||
value: 5,
|
||||
label: '5m',
|
||||
},
|
||||
{
|
||||
value: 15,
|
||||
label: '15m',
|
||||
},
|
||||
{
|
||||
value: 30,
|
||||
label: '30m',
|
||||
},
|
||||
{
|
||||
value: 60,
|
||||
label: '1hr',
|
||||
},
|
||||
{
|
||||
value: 360,
|
||||
label: '6hrs',
|
||||
},
|
||||
{
|
||||
value: 720,
|
||||
label: '12hrs',
|
||||
},
|
||||
];
|
||||
|
||||
function LogLiveTail(): JSX.Element {
|
||||
const {
|
||||
liveTail,
|
||||
searchFilter: { queryString },
|
||||
liveTailStartRange,
|
||||
logs,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
const dispatch = useDispatch();
|
||||
const handleLiveTail = (toggleState: TLogsLiveTailState): void => {
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: toggleState,
|
||||
});
|
||||
};
|
||||
|
||||
const batchedEventsRef = useRef<Record<string, unknown>[]>([]);
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const pushLiveLog = useCallback(
|
||||
throttle(() => {
|
||||
dispatch({
|
||||
type: PUSH_LIVE_TAIL_EVENT,
|
||||
payload: batchedEventsRef.current.reverse(),
|
||||
});
|
||||
// console.log('DISPATCH', batchedEventsRef.current.length);
|
||||
batchedEventsRef.current = [];
|
||||
}, 1500),
|
||||
[],
|
||||
);
|
||||
|
||||
const batchLiveLog = (e: { data: string }): void => {
|
||||
// console.log('EVENT BATCHED');
|
||||
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
|
||||
pushLiveLog();
|
||||
};
|
||||
|
||||
// This ref depicts thats whether the live tail is played from paused state or not.
|
||||
const liveTailSourceRef = useRef<EventSource | null>(null);
|
||||
useEffect(() => {
|
||||
if (liveTail === 'PLAYING') {
|
||||
// console.log('Starting Live Tail', logs.length);
|
||||
const timeStamp = dayjs().subtract(liveTailStartRange, 'minute').valueOf();
|
||||
const queryParams = new URLSearchParams({
|
||||
...(queryString ? { q: queryString } : {}),
|
||||
timestampStart: (timeStamp * 1e6) as never,
|
||||
...(liveTailSourceRef.current && logs.length > 0
|
||||
? {
|
||||
idGt: logs[0].id,
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
const source = LiveTail(queryParams.toString());
|
||||
liveTailSourceRef.current = source;
|
||||
source.onmessage = function connectionMessage(e): void {
|
||||
batchLiveLog(e);
|
||||
};
|
||||
// source.onopen = function connectionOpen(): void { };
|
||||
source.onerror = function connectionError(event: unknown): void {
|
||||
console.error(event);
|
||||
source.close();
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: false,
|
||||
});
|
||||
};
|
||||
} else if (liveTailSourceRef.current && liveTailSourceRef.current.close) {
|
||||
liveTailSourceRef.current?.close();
|
||||
}
|
||||
|
||||
if (liveTail === 'STOPPED') {
|
||||
liveTailSourceRef.current = null;
|
||||
}
|
||||
}, [liveTail]);
|
||||
|
||||
const handleLiveTailStart = (): void => {
|
||||
handleLiveTail('PLAYING');
|
||||
if (!liveTailSourceRef.current) {
|
||||
dispatch({
|
||||
type: FLUSH_LOGS,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const OptionsPopOverContent = useMemo(
|
||||
() => (
|
||||
<TimePickerSelect
|
||||
disabled={liveTail === 'PLAYING'}
|
||||
value={liveTailStartRange}
|
||||
onChange={(value): void => {
|
||||
dispatch({
|
||||
type: SET_LIVE_TAIL_START_TIME,
|
||||
payload: value,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{TIME_PICKER_OPTIONS.map((optionData) => (
|
||||
<Option key={optionData.label} value={optionData.value}>
|
||||
Last {optionData.label}
|
||||
</Option>
|
||||
))}
|
||||
</TimePickerSelect>
|
||||
),
|
||||
[dispatch, liveTail, liveTailStartRange],
|
||||
);
|
||||
return (
|
||||
<TimePickerCard>
|
||||
<Row
|
||||
style={{ gap: '0.5rem', alignItems: 'center', justifyContent: 'center' }}
|
||||
>
|
||||
<div>
|
||||
{liveTail === 'PLAYING' ? (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(): void => handleLiveTail('PAUSED')}
|
||||
title="Pause live tail"
|
||||
style={{ background: green[6] }}
|
||||
>
|
||||
Pause <PauseOutlined />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleLiveTailStart}
|
||||
title="Start live tail"
|
||||
>
|
||||
Go Live <PlayCircleOutlined />
|
||||
</Button>
|
||||
)}
|
||||
{liveTail !== 'STOPPED' && (
|
||||
<Button
|
||||
type="dashed"
|
||||
onClick={(): void => handleLiveTail('STOPPED')}
|
||||
title="Exit live tail"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
height: '0.8rem',
|
||||
width: '0.8rem',
|
||||
background: isDarkMode ? '#eee' : '#222',
|
||||
borderRadius: '0.1rem',
|
||||
}}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Popover
|
||||
placement="bottomRight"
|
||||
title="Select Live Tail Timing"
|
||||
trigger="click"
|
||||
content={OptionsPopOverContent}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
padding: '0.3rem 0.4rem 0.3rem 0',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignContent: 'center',
|
||||
}}
|
||||
>
|
||||
<OptionIcon isDarkMode={isDarkMode} />
|
||||
</span>
|
||||
</Popover>
|
||||
</Row>
|
||||
</TimePickerCard>
|
||||
);
|
||||
}
|
||||
|
||||
export default LogLiveTail;
|
12
frontend/src/container/LogLiveTail/styles.ts
Normal file
12
frontend/src/container/LogLiveTail/styles.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Card, Select } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const TimePickerCard = styled(Card)`
|
||||
.ant-card-body {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const TimePickerSelect = styled(Select)`
|
||||
min-width: 100px;
|
||||
`;
|
71
frontend/src/container/Logs/index.tsx
Normal file
71
frontend/src/container/Logs/index.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { Divider, Row } from 'antd';
|
||||
import LogControls from 'container/LogControls';
|
||||
import LogDetailedView from 'container/LogDetailedView';
|
||||
import LogLiveTail from 'container/LogLiveTail';
|
||||
import LogsAggregate from 'container/LogsAggregate';
|
||||
import LogsFilters from 'container/LogsFilters';
|
||||
import SearchFilter from 'container/LogsSearchFilter';
|
||||
import LogsTable from 'container/LogsTable';
|
||||
import React, { memo, useEffect, useMemo } from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { GetLogsFields } from 'store/actions/logs/getFields';
|
||||
import AppActions from 'types/actions';
|
||||
import { SET_SEARCH_QUERY_STRING } from 'types/actions/logs';
|
||||
|
||||
interface LogsProps {
|
||||
getLogsFields: VoidFunction;
|
||||
}
|
||||
function Logs({ getLogsFields }: LogsProps): JSX.Element {
|
||||
const { search } = useLocation();
|
||||
|
||||
const urlQuery = useMemo(() => {
|
||||
return new URLSearchParams(search);
|
||||
}, [search]);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_STRING,
|
||||
payload: urlQuery.get('q'),
|
||||
});
|
||||
}, [dispatch, urlQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
getLogsFields();
|
||||
}, [getLogsFields]);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Row style={{ justifyContent: 'center', alignItems: 'center' }}>
|
||||
<SearchFilter />
|
||||
<Divider type="vertical" style={{ height: '2rem' }} />
|
||||
<LogLiveTail />
|
||||
</Row>
|
||||
<LogsAggregate />
|
||||
<LogControls />
|
||||
<Divider style={{ margin: 0 }} />
|
||||
<Row gutter={20} style={{ flexWrap: 'nowrap' }}>
|
||||
<LogsFilters />
|
||||
<Divider type="vertical" style={{ height: '100%', margin: 0 }} />
|
||||
<LogsTable />
|
||||
</Row>
|
||||
<LogDetailedView />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogsFields: () => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(Logs));
|
133
frontend/src/container/LogsAggregate/index.tsx
Normal file
133
frontend/src/container/LogsAggregate/index.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { blue } from '@ant-design/colors';
|
||||
import Graph from 'components/Graph';
|
||||
import Spinner from 'components/Spinner';
|
||||
import dayjs from 'dayjs';
|
||||
import getStep from 'lib/getStep';
|
||||
import React, { memo, useEffect, useRef } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { Container } from './styles';
|
||||
|
||||
interface LogsAggregateProps {
|
||||
getLogsAggregate: (arg0: Parameters<typeof getLogsAggregate>[0]) => void;
|
||||
}
|
||||
function LogsAggregate({ getLogsAggregate }: LogsAggregateProps): JSX.Element {
|
||||
const {
|
||||
searchFilter: { queryString },
|
||||
idEnd,
|
||||
idStart,
|
||||
isLoadingAggregate,
|
||||
logsAggregate,
|
||||
liveTail,
|
||||
liveTailStartRange,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const reFetchIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
useEffect(() => {
|
||||
switch (liveTail) {
|
||||
case 'STOPPED': {
|
||||
if (reFetchIntervalRef.current) {
|
||||
clearInterval(reFetchIntervalRef.current);
|
||||
}
|
||||
reFetchIntervalRef.current = null;
|
||||
getLogsAggregate({
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
step: getStep({
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: queryString,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'PLAYING': {
|
||||
const aggregateCall = (): void => {
|
||||
const startTime =
|
||||
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
|
||||
const endTime = dayjs().valueOf() * 1e6;
|
||||
getLogsAggregate({
|
||||
timestampStart: startTime,
|
||||
timestampEnd: endTime,
|
||||
step: getStep({
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: queryString,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
};
|
||||
aggregateCall();
|
||||
reFetchIntervalRef.current = setInterval(aggregateCall, 60000);
|
||||
break;
|
||||
}
|
||||
case 'PAUSED': {
|
||||
if (reFetchIntervalRef.current) {
|
||||
clearInterval(reFetchIntervalRef.current);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [getLogsAggregate, maxTime, minTime, liveTail]);
|
||||
|
||||
const data = {
|
||||
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
|
||||
datasets: [
|
||||
{
|
||||
data: logsAggregate.map((s) => s.value),
|
||||
backgroundColor: blue[4],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{isLoadingAggregate ? (
|
||||
<Spinner size="default" height="100%" />
|
||||
) : (
|
||||
<Graph
|
||||
name="usage"
|
||||
data={data}
|
||||
type="bar"
|
||||
containerHeight="100%"
|
||||
animate={false}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogsAggregate: (
|
||||
props: Parameters<typeof getLogsAggregate>[0],
|
||||
) => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(LogsAggregate));
|
11
frontend/src/container/LogsAggregate/styles.ts
Normal file
11
frontend/src/container/LogsAggregate/styles.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Card } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled(Card)`
|
||||
position: relative;
|
||||
margin: 0.5rem 0;
|
||||
.ant-card-body {
|
||||
height: 20vh;
|
||||
min-height: 200px;
|
||||
}
|
||||
`;
|
57
frontend/src/container/LogsFilters/FieldItem.tsx
Normal file
57
frontend/src/container/LogsFilters/FieldItem.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { Button, Popover, Spin } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
|
||||
import { Field } from './styles';
|
||||
|
||||
interface FieldItemProps {
|
||||
name: string;
|
||||
buttonIcon: React.ReactNode;
|
||||
buttonOnClick: (arg0: Record<string, unknown>) => void;
|
||||
fieldData: Record<string, never>;
|
||||
fieldIndex: number;
|
||||
isLoading: boolean;
|
||||
iconHoverText: string;
|
||||
}
|
||||
export function FieldItem({
|
||||
name,
|
||||
buttonIcon,
|
||||
buttonOnClick,
|
||||
fieldData,
|
||||
fieldIndex,
|
||||
isLoading,
|
||||
iconHoverText,
|
||||
}: FieldItemProps): JSX.Element {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const { isDarkMode } = useSelector<AppState, AppReducer>((state) => state.app);
|
||||
return (
|
||||
<Field
|
||||
onMouseEnter={(): void => {
|
||||
setIsHovered(true);
|
||||
}}
|
||||
onMouseLeave={(): void => setIsHovered(false)}
|
||||
isDarkMode={isDarkMode}
|
||||
>
|
||||
<span>{name}</span>
|
||||
{isLoading ? (
|
||||
<Spin spinning size="small" indicator={<LoadingOutlined spin />} />
|
||||
) : (
|
||||
isHovered &&
|
||||
buttonOnClick && (
|
||||
<Popover content={<span>{iconHoverText}</span>}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={buttonIcon}
|
||||
onClick={(): void => buttonOnClick({ fieldData, fieldIndex })}
|
||||
style={{ color: 'inherit', padding: 0, height: '1rem', width: '1rem' }}
|
||||
/>
|
||||
</Popover>
|
||||
)
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
}
|
153
frontend/src/container/LogsFilters/index.tsx
Normal file
153
frontend/src/container/LogsFilters/index.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import { red } from '@ant-design/colors';
|
||||
import { CloseOutlined, PlusCircleFilled } from '@ant-design/icons';
|
||||
import { Input } from 'antd';
|
||||
import AddToSelectedFields from 'api/logs/AddToSelectedField';
|
||||
import RemoveSelectedField from 'api/logs/RemoveFromSelectedField';
|
||||
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
|
||||
import React, { memo, useState } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { GetLogsFields } from 'store/actions/logs/getFields';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { IInterestingFields, ISelectedFields } from 'types/api/logs/fields';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { FieldItem } from './FieldItem';
|
||||
import { CategoryContainer, Container, FieldContainer } from './styles';
|
||||
|
||||
const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
|
||||
|
||||
interface LogsFiltersProps {
|
||||
getLogsFields: () => void;
|
||||
}
|
||||
function LogsFilters({ getLogsFields }: LogsFiltersProps): JSX.Element {
|
||||
const {
|
||||
fields: { interesting, selected },
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
|
||||
const [selectedFieldLoading, setSelectedFieldLoading] = useState<number[]>([]);
|
||||
const [interestingFieldLoading, setInterestingFieldLoading] = useState<
|
||||
number[]
|
||||
>([]);
|
||||
|
||||
const [filterValuesInput, setFilterValuesInput] = useState('');
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setFilterValuesInput((e.target as HTMLInputElement).value);
|
||||
};
|
||||
|
||||
const handleAddInterestingToSelected = async ({
|
||||
fieldData,
|
||||
fieldIndex,
|
||||
}: {
|
||||
fieldData: IInterestingFields;
|
||||
fieldIndex: number;
|
||||
}): Promise<void> => {
|
||||
setInterestingFieldLoading((prevState: number[]) => {
|
||||
prevState.push(fieldIndex);
|
||||
return [...prevState];
|
||||
});
|
||||
|
||||
await AddToSelectedFields({
|
||||
...fieldData,
|
||||
selected: true,
|
||||
});
|
||||
getLogsFields();
|
||||
|
||||
setInterestingFieldLoading(
|
||||
interestingFieldLoading.filter((e) => e !== fieldIndex),
|
||||
);
|
||||
};
|
||||
const handleRemoveSelectedField = async ({
|
||||
fieldData,
|
||||
fieldIndex,
|
||||
}: {
|
||||
fieldData: ISelectedFields;
|
||||
fieldIndex: number;
|
||||
}): Promise<void> => {
|
||||
setSelectedFieldLoading((prevState) => {
|
||||
prevState.push(fieldIndex);
|
||||
return [...prevState];
|
||||
});
|
||||
|
||||
await RemoveSelectedField({
|
||||
...fieldData,
|
||||
selected: false,
|
||||
});
|
||||
|
||||
getLogsFields();
|
||||
|
||||
setSelectedFieldLoading(
|
||||
interestingFieldLoading.filter((e) => e !== fieldIndex),
|
||||
);
|
||||
};
|
||||
return (
|
||||
<Container flex="450px">
|
||||
<Input
|
||||
placeholder="Filter Values"
|
||||
onInput={handleSearch}
|
||||
style={{ width: '100%' }}
|
||||
value={filterValuesInput}
|
||||
onChange={handleSearch}
|
||||
/>
|
||||
|
||||
<CategoryContainer>
|
||||
<CategoryHeading>SELECTED FIELDS</CategoryHeading>
|
||||
<FieldContainer>
|
||||
{selected
|
||||
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
|
||||
.map((field, idx) => (
|
||||
<FieldItem
|
||||
key={`${JSON.stringify(field)}-${idx}`}
|
||||
name={field.name}
|
||||
fieldData={field as never}
|
||||
fieldIndex={idx}
|
||||
buttonIcon={<CloseOutlined style={{ color: red[5] }} />}
|
||||
buttonOnClick={
|
||||
(!RESTRICTED_SELECTED_FIELDS.includes(field.name) &&
|
||||
handleRemoveSelectedField) as never
|
||||
}
|
||||
isLoading={selectedFieldLoading.includes(idx)}
|
||||
iconHoverText="Remove from Selected Fields"
|
||||
/>
|
||||
))}
|
||||
</FieldContainer>
|
||||
</CategoryContainer>
|
||||
<CategoryContainer>
|
||||
<CategoryHeading>INTERESTING FIELDS</CategoryHeading>
|
||||
<FieldContainer>
|
||||
{interesting
|
||||
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
|
||||
.map((field, idx) => (
|
||||
<FieldItem
|
||||
key={`${JSON.stringify(field)}-${idx}`}
|
||||
name={field.name}
|
||||
fieldData={field as never}
|
||||
fieldIndex={idx}
|
||||
buttonIcon={<PlusCircleFilled />}
|
||||
buttonOnClick={handleAddInterestingToSelected as never}
|
||||
isLoading={interestingFieldLoading.includes(idx)}
|
||||
iconHoverText="Add to Selected Fields"
|
||||
/>
|
||||
))}
|
||||
</FieldContainer>
|
||||
</CategoryContainer>
|
||||
{/* <ExtractField>Extract Fields</ExtractField> */}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogsFields: () => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(LogsFilters));
|
36
frontend/src/container/LogsFilters/styles.ts
Normal file
36
frontend/src/container/LogsFilters/styles.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { blue, grey } from '@ant-design/colors';
|
||||
import { Col, Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled(Col)`
|
||||
padding-top: 0.3rem;
|
||||
min-width: 250px;
|
||||
max-width: 350px;
|
||||
`;
|
||||
|
||||
export const CategoryContainer = styled.div`
|
||||
margin: 1rem 0;
|
||||
padding-left: 0.2rem;
|
||||
`;
|
||||
|
||||
export const FieldContainer = styled(Typography.Text)`
|
||||
margin: 0.2rem 0;
|
||||
color: ${blue[4]};
|
||||
`;
|
||||
|
||||
export const Field = styled.div<{ isDarkMode: boolean }>`
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.3rem 0.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
&:hover {
|
||||
background: ${({ isDarkMode }): string => {
|
||||
return isDarkMode ? grey[7] : '#ddd';
|
||||
}};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ExtractField = styled(Typography.Text)`
|
||||
color: ${blue[4]};
|
||||
`;
|
@ -0,0 +1,20 @@
|
||||
import { Typography } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
interface FieldKeyProps {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
function FieldKey({ name, type }: FieldKeyProps): JSX.Element {
|
||||
return (
|
||||
<span style={{ margin: '0.25rem 0', display: 'flex', gap: '0.5rem' }}>
|
||||
<Typography.Text>{name}</Typography.Text>
|
||||
<Typography.Text type="secondary" italic>
|
||||
{type}
|
||||
</Typography.Text>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default FieldKey;
|
@ -0,0 +1,263 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-bitwise */
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Select } from 'antd';
|
||||
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||
import {
|
||||
ConditionalOperators,
|
||||
QueryOperatorsMultiVal,
|
||||
QueryOperatorsSingleVal,
|
||||
} from 'lib/logql/tokens';
|
||||
import { flatten } from 'lodash-es';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import FieldKey from '../FieldKey';
|
||||
import { QueryConditionContainer, QueryFieldContainer } from '../styles';
|
||||
import { createParsedQueryStructure } from '../utils';
|
||||
|
||||
const { Option } = Select;
|
||||
interface QueryFieldProps {
|
||||
query: { value: string | string[]; type: string }[];
|
||||
queryIndex: number;
|
||||
onUpdate: (query: unknown, queryIndex: number) => void;
|
||||
onDelete: (queryIndex: number) => void;
|
||||
}
|
||||
function QueryField({
|
||||
query,
|
||||
queryIndex,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}: QueryFieldProps): JSX.Element | null {
|
||||
const {
|
||||
fields: { selected },
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
|
||||
const getFieldType = (inputKey: string): string => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const selectedField of selected) {
|
||||
if (inputKey === selectedField.name) {
|
||||
return selectedField.type;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const fieldType = useMemo(() => getFieldType(query[0].value as string), [
|
||||
query,
|
||||
]);
|
||||
const handleChange = (qIdx: number, value: string): void => {
|
||||
query[qIdx].value = value || '';
|
||||
|
||||
if (qIdx === 1) {
|
||||
if (Object.values(QueryOperatorsMultiVal).includes(value)) {
|
||||
if (!Array.isArray(query[2].value)) {
|
||||
query[2].value = [];
|
||||
}
|
||||
} else if (
|
||||
Object.values(QueryOperatorsSingleVal).includes(value) &&
|
||||
Array.isArray(query[2].value)
|
||||
) {
|
||||
query[2].value = '';
|
||||
}
|
||||
}
|
||||
onUpdate(query, queryIndex);
|
||||
};
|
||||
|
||||
const handleClear = (): void => {
|
||||
onDelete(queryIndex);
|
||||
};
|
||||
if (!Array.isArray(query)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueryFieldContainer
|
||||
style={{ ...(queryIndex === 0 && { gridColumnStart: 2 }) }}
|
||||
>
|
||||
<div style={{ flex: 1, minWidth: 100 }}>
|
||||
<FieldKey name={(query[0] && query[0].value) as string} type={fieldType} />
|
||||
</div>
|
||||
<Select
|
||||
defaultActiveFirstOption={false}
|
||||
placeholder="Select Operator"
|
||||
defaultValue={
|
||||
query[1] && query[1].value
|
||||
? (query[1].value as string).toUpperCase()
|
||||
: null
|
||||
}
|
||||
onChange={(e): void => handleChange(1, e)}
|
||||
style={{ minWidth: 150 }}
|
||||
>
|
||||
{Object.values({
|
||||
...QueryOperatorsMultiVal,
|
||||
...QueryOperatorsSingleVal,
|
||||
}).map((cond) => (
|
||||
<Option key={cond} value={cond} label={cond}>
|
||||
{cond}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<div style={{ flex: 2 }}>
|
||||
{Array.isArray(query[2].value) ||
|
||||
Object.values(QueryOperatorsMultiVal).some(
|
||||
(op) => op.toUpperCase() === (query[1].value as string)?.toUpperCase(),
|
||||
) ? (
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
onChange={(e): void => handleChange(2, e as never)}
|
||||
defaultValue={(query[2] && query[2].value) || []}
|
||||
notFoundContent={null}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
onChange={(e): void => handleChange(2, e.target.value)}
|
||||
style={{ width: '100%' }}
|
||||
defaultValue={query[2] && query[2].value}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
icon={<CloseOutlined />}
|
||||
type="text"
|
||||
size="small"
|
||||
onClick={handleClear}
|
||||
/>
|
||||
</QueryFieldContainer>
|
||||
);
|
||||
}
|
||||
|
||||
interface QueryConditionFieldProps {
|
||||
query: { value: string | string[]; type: string }[];
|
||||
queryIndex: number;
|
||||
onUpdate: (arg0: unknown, arg1: number) => void;
|
||||
}
|
||||
function QueryConditionField({
|
||||
query,
|
||||
queryIndex,
|
||||
onUpdate,
|
||||
}: QueryConditionFieldProps): JSX.Element {
|
||||
return (
|
||||
<QueryConditionContainer>
|
||||
<Select
|
||||
defaultValue={
|
||||
(query as any).value &&
|
||||
(((query as any)?.value as any) as string).toUpperCase()
|
||||
}
|
||||
onChange={(e): void => {
|
||||
onUpdate({ ...query, value: e }, queryIndex);
|
||||
}}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{Object.values(ConditionalOperators).map((cond) => (
|
||||
<Option key={cond} value={cond} label={cond}>
|
||||
{cond}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
</QueryConditionContainer>
|
||||
);
|
||||
}
|
||||
const hashCode = (s: string): string => {
|
||||
if (!s) {
|
||||
return '0';
|
||||
}
|
||||
return `${Math.abs(
|
||||
s.split('').reduce((a, b) => {
|
||||
a = (a << 5) - a + b.charCodeAt(0);
|
||||
return a & a;
|
||||
}, 0),
|
||||
)}`;
|
||||
};
|
||||
|
||||
function QueryBuilder({
|
||||
updateParsedQuery,
|
||||
}: {
|
||||
updateParsedQuery: (arg0: unknown) => void;
|
||||
}): JSX.Element {
|
||||
const {
|
||||
searchFilter: { parsedQuery },
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
|
||||
const keyPrefixRef = useRef(hashCode(JSON.stringify(parsedQuery)));
|
||||
const [keyPrefix, setKeyPrefix] = useState(keyPrefixRef.current);
|
||||
const generatedQueryStructure = createParsedQueryStructure(
|
||||
parsedQuery as never[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const incomingHashCode = hashCode(JSON.stringify(parsedQuery));
|
||||
if (incomingHashCode !== keyPrefixRef.current) {
|
||||
keyPrefixRef.current = incomingHashCode;
|
||||
setKeyPrefix(incomingHashCode);
|
||||
}
|
||||
}, [parsedQuery]);
|
||||
|
||||
const handleUpdate = (
|
||||
query: { value: string | string[]; type: string }[],
|
||||
queryIndex: number,
|
||||
): void => {
|
||||
const updatedParsedQuery = generatedQueryStructure;
|
||||
updatedParsedQuery[queryIndex] = query as never;
|
||||
|
||||
const flatParsedQuery = flatten(updatedParsedQuery).filter((q) => q.value);
|
||||
keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery));
|
||||
updateParsedQuery(flatParsedQuery);
|
||||
};
|
||||
|
||||
const handleDelete = (queryIndex: number): void => {
|
||||
const updatedParsedQuery = generatedQueryStructure;
|
||||
updatedParsedQuery.splice(queryIndex - 1, 2);
|
||||
|
||||
const flatParsedQuery = flatten(updatedParsedQuery).filter((q) => q.value);
|
||||
keyPrefixRef.current = v4();
|
||||
updateParsedQuery(flatParsedQuery);
|
||||
};
|
||||
|
||||
const QueryUI = (): JSX.Element | JSX.Element[] =>
|
||||
generatedQueryStructure.map((query, idx) => {
|
||||
if (Array.isArray(query))
|
||||
return (
|
||||
<QueryField
|
||||
key={keyPrefix + idx}
|
||||
query={query as never}
|
||||
queryIndex={idx}
|
||||
onUpdate={handleUpdate as never}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryConditionField
|
||||
key={keyPrefix + idx}
|
||||
query={query}
|
||||
queryIndex={idx}
|
||||
onUpdate={handleUpdate as never}
|
||||
/>
|
||||
);
|
||||
});
|
||||
return (
|
||||
<div>
|
||||
<CategoryHeading>LOG QUERY BUILDER</CategoryHeading>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '80px 1fr',
|
||||
margin: '0.5rem 0',
|
||||
}}
|
||||
>
|
||||
{QueryUI()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default QueryBuilder;
|
@ -0,0 +1,57 @@
|
||||
import { Button } from 'antd';
|
||||
import CategoryHeading from 'components/Logs/CategoryHeading';
|
||||
import { map } from 'lodash-es';
|
||||
import React from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { ADD_SEARCH_FIELD_QUERY_STRING } from 'types/actions/logs';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import FieldKey from './FieldKey';
|
||||
|
||||
interface SuggestedItemProps {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
function SuggestedItem({ name, type }: SuggestedItemProps): JSX.Element {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const addSuggestedField = (): void => {
|
||||
dispatch({
|
||||
type: ADD_SEARCH_FIELD_QUERY_STRING,
|
||||
payload: name,
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Button
|
||||
type="text"
|
||||
style={{ display: 'block', padding: '0.2rem' }}
|
||||
onClick={addSuggestedField}
|
||||
>
|
||||
<FieldKey name={name} type={type} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function Suggestions(): JSX.Element {
|
||||
const {
|
||||
fields: { selected },
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CategoryHeading>SUGGESTIONS</CategoryHeading>
|
||||
<div>
|
||||
{map(selected, (field) => (
|
||||
<SuggestedItem
|
||||
key={JSON.stringify(field)}
|
||||
name={field.name}
|
||||
type={field.type}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Suggestions;
|
@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
|
||||
import QueryBuilder from './QueryBuilder/QueryBuilder';
|
||||
import Suggestions from './Suggestions';
|
||||
|
||||
interface SearchFieldsProps {
|
||||
updateParsedQuery: () => void;
|
||||
}
|
||||
function SearchFields({ updateParsedQuery }: SearchFieldsProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<QueryBuilder updateParsedQuery={updateParsedQuery} />
|
||||
<Suggestions />
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default SearchFields;
|
@ -0,0 +1,20 @@
|
||||
import { blue } from '@ant-design/colors';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const QueryFieldContainer = styled.div`
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.1rem 0.5rem 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-radius: 0.25rem;
|
||||
gap: 1rem;
|
||||
&:hover {
|
||||
background: ${blue[6]};
|
||||
}
|
||||
`;
|
||||
|
||||
export const QueryConditionContainer = styled.div`
|
||||
padding: 0.25rem 0rem;
|
||||
margin: 0.1rem 0;
|
||||
`;
|
@ -0,0 +1,61 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
// @ts-nocheck
|
||||
|
||||
import { QueryTypes } from 'lib/logql/tokens';
|
||||
|
||||
export const queryKOVPair = () => [
|
||||
{
|
||||
type: QueryTypes.QUERY_KEY,
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_OPERATOR,
|
||||
value: null,
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_VALUE,
|
||||
value: null,
|
||||
},
|
||||
];
|
||||
export const createParsedQueryStructure = (parsedQuery = []) => {
|
||||
if (!parsedQuery.length) {
|
||||
return parsedQuery;
|
||||
}
|
||||
|
||||
const structuredArray = [queryKOVPair()];
|
||||
|
||||
let cond;
|
||||
let qCtr = -1;
|
||||
parsedQuery.forEach((query, idx) => {
|
||||
if (cond) {
|
||||
structuredArray.push(cond);
|
||||
structuredArray.push(queryKOVPair());
|
||||
cond = null;
|
||||
qCtr = -1;
|
||||
}
|
||||
const stagingArr = structuredArray[structuredArray.length - 1];
|
||||
const prevQuery =
|
||||
Array.isArray(stagingArr) && qCtr >= 0 ? stagingArr[qCtr] : null;
|
||||
|
||||
if (query.type === QueryTypes.QUERY_KEY) {
|
||||
stagingArr[qCtr + 1] = query;
|
||||
} else if (
|
||||
query.type === QueryTypes.QUERY_OPERATOR &&
|
||||
prevQuery &&
|
||||
prevQuery.type === QueryTypes.QUERY_KEY
|
||||
) {
|
||||
stagingArr[qCtr + 1] = query;
|
||||
} else if (
|
||||
query.type === QueryTypes.QUERY_VALUE &&
|
||||
prevQuery &&
|
||||
prevQuery.type === QueryTypes.QUERY_OPERATOR
|
||||
) {
|
||||
stagingArr[qCtr + 1] = query;
|
||||
} else if (query.type === QueryTypes.CONDITIONAL_OPERATOR) {
|
||||
cond = query;
|
||||
}
|
||||
qCtr++;
|
||||
});
|
||||
return structuredArray;
|
||||
};
|
178
frontend/src/container/LogsSearchFilter/index.tsx
Normal file
178
frontend/src/container/LogsSearchFilter/index.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { CloseSquareOutlined } from '@ant-design/icons';
|
||||
import { Button, Input } from 'antd';
|
||||
import useClickOutside from 'hooks/useClickOutside';
|
||||
import getStep from 'lib/getStep';
|
||||
import React, { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { connect, useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-use';
|
||||
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 { TOGGLE_LIVE_TAIL } from 'types/actions/logs';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import SearchFields from './SearchFields';
|
||||
import { DropDownContainer } from './styles';
|
||||
import { useSearchParser } from './useSearchParser';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
interface SearchFilterProps {
|
||||
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||
getLogsAggregate: (
|
||||
props: Parameters<typeof getLogsAggregate>[0],
|
||||
) => ReturnType<typeof getLogsAggregate>;
|
||||
}
|
||||
function SearchFilter({
|
||||
getLogs,
|
||||
getLogsAggregate,
|
||||
}: SearchFilterProps): JSX.Element {
|
||||
const {
|
||||
queryString,
|
||||
updateParsedQuery,
|
||||
updateQueryString,
|
||||
} = useSearchParser();
|
||||
const [showDropDown, setShowDropDown] = useState(false);
|
||||
|
||||
const { logLinesPerPage, idEnd, idStart, liveTail } = useSelector<
|
||||
AppState,
|
||||
ILogsReducer
|
||||
>((state) => state.logs);
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
const searchComponentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useClickOutside(searchComponentRef, (e: HTMLElement) => {
|
||||
// using this hack as overlay span is voilating this condition
|
||||
if (
|
||||
e.nodeName === 'svg' ||
|
||||
e.nodeName === 'path' ||
|
||||
e.nodeName === 'span' ||
|
||||
e.nodeName === 'button'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
e.nodeName === 'DIV' &&
|
||||
![
|
||||
'ant-empty-image',
|
||||
'ant-select-item',
|
||||
'ant-col',
|
||||
'ant-select-item-option-content',
|
||||
'ant-select-item-option-active',
|
||||
].find((p) => p.indexOf(e.className) !== -1) &&
|
||||
!(e.ariaSelected === 'true') &&
|
||||
showDropDown
|
||||
) {
|
||||
setShowDropDown(false);
|
||||
}
|
||||
});
|
||||
const { search } = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const handleSearch = (customQuery = ''): void => {
|
||||
if (liveTail === 'PLAYING') {
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: 'PAUSED',
|
||||
});
|
||||
setTimeout(
|
||||
() =>
|
||||
dispatch({
|
||||
type: TOGGLE_LIVE_TAIL,
|
||||
payload: liveTail,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
} else {
|
||||
getLogs({
|
||||
q: customQuery || queryString,
|
||||
limit: logLinesPerPage,
|
||||
orderBy: 'timestamp',
|
||||
order: 'desc',
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
|
||||
getLogsAggregate({
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
step: getStep({
|
||||
start: minTime,
|
||||
end: maxTime,
|
||||
inputFormat: 'ns',
|
||||
}),
|
||||
q: customQuery || queryString,
|
||||
});
|
||||
}
|
||||
setShowDropDown(false);
|
||||
};
|
||||
|
||||
const urlQuery = useMemo(() => {
|
||||
return new URLSearchParams(search);
|
||||
}, [search]);
|
||||
|
||||
useEffect(() => {
|
||||
const urlQueryString = urlQuery.get('q');
|
||||
if (urlQueryString !== null) handleSearch(urlQueryString);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={searchComponentRef} style={{ flex: 1 }}>
|
||||
<Search
|
||||
placeholder="Search Filter"
|
||||
onFocus={(): void => setShowDropDown(true)}
|
||||
value={queryString}
|
||||
onChange={(e): void => {
|
||||
updateQueryString(e.target.value);
|
||||
}}
|
||||
onSearch={handleSearch}
|
||||
/>
|
||||
<div style={{ position: 'relative' }}>
|
||||
{showDropDown && (
|
||||
<DropDownContainer>
|
||||
<Button
|
||||
type="text"
|
||||
onClick={(): void => setShowDropDown(false)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<CloseSquareOutlined />
|
||||
</Button>
|
||||
<SearchFields updateParsedQuery={updateParsedQuery as never} />
|
||||
</DropDownContainer>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
interface DispatchProps {
|
||||
getLogs: (
|
||||
props: Parameters<typeof getLogs>[0],
|
||||
) => (dispatch: Dispatch<AppActions>) => void;
|
||||
getLogsAggregate: (
|
||||
props: Parameters<typeof getLogsAggregate>[0],
|
||||
) => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogs: bindActionCreators(getLogs, dispatch),
|
||||
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(SearchFilter));
|
12
frontend/src/container/LogsSearchFilter/styles.ts
Normal file
12
frontend/src/container/LogsSearchFilter/styles.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Card } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const DropDownContainer = styled(Card)`
|
||||
top: 0.5rem;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
.ant-card-body {
|
||||
padding: 0.8rem;
|
||||
}
|
||||
`;
|
76
frontend/src/container/LogsSearchFilter/useSearchParser.ts
Normal file
76
frontend/src/container/LogsSearchFilter/useSearchParser.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import history from 'lib/history';
|
||||
import { parseQuery, reverseParser } from 'lib/logql';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import {
|
||||
SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
||||
SET_SEARCH_QUERY_STRING,
|
||||
} from 'types/actions/logs';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
export function useSearchParser(): {
|
||||
queryString: string;
|
||||
parsedQuery: unknown;
|
||||
updateParsedQuery: (arg0: unknown) => void;
|
||||
updateQueryString: (arg0: unknown) => void;
|
||||
} {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
searchFilter: { parsedQuery, queryString },
|
||||
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
|
||||
|
||||
const updateQueryString = useCallback(
|
||||
(updatedQueryString) => {
|
||||
history.push({
|
||||
pathname: history.location.pathname,
|
||||
search: updatedQueryString ? `?q=${updatedQueryString}` : '',
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_STRING,
|
||||
payload: updatedQueryString,
|
||||
});
|
||||
const parsedQueryFromString = parseQuery(updatedQueryString);
|
||||
if (!isEqual(parsedQuery, parsedQueryFromString)) {
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
||||
payload: parsedQueryFromString,
|
||||
});
|
||||
}
|
||||
},
|
||||
[dispatch, parsedQuery],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryString !== null) updateQueryString(queryString);
|
||||
}, [queryString, updateQueryString]);
|
||||
|
||||
const updateParsedQuery = useCallback(
|
||||
(updatedParsedPayload) => {
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
||||
payload: updatedParsedPayload,
|
||||
});
|
||||
const reversedParsedQuery = reverseParser(updatedParsedPayload);
|
||||
if (
|
||||
!isEqual(queryString, reversedParsedQuery) ||
|
||||
(queryString === '' && reversedParsedQuery === '')
|
||||
) {
|
||||
dispatch({
|
||||
type: SET_SEARCH_QUERY_STRING,
|
||||
payload: reversedParsedQuery,
|
||||
});
|
||||
}
|
||||
},
|
||||
[dispatch, queryString],
|
||||
);
|
||||
|
||||
return {
|
||||
queryString,
|
||||
parsedQuery,
|
||||
updateParsedQuery,
|
||||
updateQueryString,
|
||||
};
|
||||
}
|
89
frontend/src/container/LogsTable/index.tsx
Normal file
89
frontend/src/container/LogsTable/index.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
/* eslint-disable no-nested-ternary */
|
||||
import { Typography } from 'antd';
|
||||
import LogItem from 'components/Logs/LogItem';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { map } from 'lodash-es';
|
||||
import React, { memo, useEffect } from 'react';
|
||||
import { connect, useSelector } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { ThunkDispatch } from 'redux-thunk';
|
||||
import { getLogs } from 'store/actions/logs/getLogs';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
import { Container, Heading } from './styles';
|
||||
|
||||
interface LogsTableProps {
|
||||
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
|
||||
}
|
||||
function LogsTable({ getLogs }: LogsTableProps): JSX.Element {
|
||||
const {
|
||||
searchFilter: { queryString },
|
||||
logs,
|
||||
logLinesPerPage,
|
||||
idEnd,
|
||||
idStart,
|
||||
isLoading,
|
||||
liveTail,
|
||||
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
|
||||
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (liveTail === 'STOPPED')
|
||||
getLogs({
|
||||
q: queryString,
|
||||
limit: logLinesPerPage,
|
||||
orderBy: 'timestamp',
|
||||
order: 'desc',
|
||||
timestampStart: minTime,
|
||||
timestampEnd: maxTime,
|
||||
...(idStart ? { idGt: idStart } : {}),
|
||||
...(idEnd ? { idLt: idEnd } : {}),
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getLogs, idEnd, idStart, logLinesPerPage, maxTime, minTime, liveTail]);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner height={20} tip="Getting Logs" />;
|
||||
}
|
||||
return (
|
||||
<Container flex="auto">
|
||||
<Heading>
|
||||
<Typography.Text
|
||||
style={{
|
||||
fontSize: '1rem',
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
Event
|
||||
</Typography.Text>
|
||||
</Heading>
|
||||
{Array.isArray(logs) && logs.length > 0 ? (
|
||||
map(logs, (log) => <LogItem key={log.id} logData={log} />)
|
||||
) : liveTail === 'PLAYING' ? (
|
||||
<span>Getting live logs...</span>
|
||||
) : (
|
||||
<span>No log lines found</span>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
interface DispatchProps {
|
||||
getLogs: (
|
||||
props: Parameters<typeof getLogs>[0],
|
||||
) => (dispatch: Dispatch<AppActions>) => void;
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (
|
||||
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
|
||||
): DispatchProps => ({
|
||||
getLogs: bindActionCreators(getLogs, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(memo(LogsTable));
|
15
frontend/src/container/LogsTable/styles.ts
Normal file
15
frontend/src/container/LogsTable/styles.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Card, Col } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled(Col)`
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
`;
|
||||
|
||||
export const Heading = styled(Card)`
|
||||
margin-bottom: 0.1rem;
|
||||
.ant-card-body {
|
||||
padding: 0.3rem 0.5rem;
|
||||
}
|
||||
`;
|
@ -1,9 +1,13 @@
|
||||
import Table, { ColumnsType } from 'antd/lib/table';
|
||||
import { blue } from '@ant-design/colors';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { Button, Input, Space, Table } from 'antd';
|
||||
import type { ColumnsType, ColumnType } from 'antd/es/table';
|
||||
import type { FilterConfirmProps } from 'antd/es/table/interface';
|
||||
import localStorageGet from 'api/browser/localstorage/get';
|
||||
import localStorageSet from 'api/browser/localstorage/set';
|
||||
import { SKIP_ONBOARDING } from 'constants/onboarding';
|
||||
import ROUTES from 'constants/routes';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -27,6 +31,57 @@ function Metrics(): JSX.Element {
|
||||
localStorageSet(SKIP_ONBOARDING, 'true');
|
||||
setSkipOnboarding(true);
|
||||
};
|
||||
const handleSearch = (confirm: (param?: FilterConfirmProps) => void): void => {
|
||||
confirm();
|
||||
};
|
||||
|
||||
const FilterIcon = useCallback(
|
||||
({ filtered }) => (
|
||||
<SearchOutlined
|
||||
style={{
|
||||
color: filtered ? blue[6] : undefined,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const filterDropdown = useCallback(
|
||||
({ setSelectedKeys, selectedKeys, confirm }) => (
|
||||
<div
|
||||
style={{
|
||||
padding: 8,
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
placeholder="Search by service"
|
||||
value={selectedKeys[0]}
|
||||
onChange={(e): void =>
|
||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
||||
}
|
||||
allowClear
|
||||
onPressEnter={(): void => handleSearch(confirm)}
|
||||
style={{
|
||||
marginBottom: 8,
|
||||
}}
|
||||
/>
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(): void => handleSearch(confirm)}
|
||||
icon={<SearchOutlined />}
|
||||
size="small"
|
||||
style={{
|
||||
width: 90,
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
if (
|
||||
services.length === 0 &&
|
||||
@ -37,21 +92,37 @@ function Metrics(): JSX.Element {
|
||||
return <SkipBoardModal onContinueClick={onContinueClick} />;
|
||||
}
|
||||
|
||||
type DataIndex = keyof ServicesList;
|
||||
|
||||
const getColumnSearchProps = (
|
||||
dataIndex: DataIndex,
|
||||
): ColumnType<DataProps> => ({
|
||||
filterDropdown,
|
||||
filterIcon: FilterIcon,
|
||||
onFilter: (value: string | number | boolean, record: DataProps): boolean =>
|
||||
record[dataIndex]
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.includes(value.toString().toLowerCase()),
|
||||
render: (text: string): JSX.Element => (
|
||||
<Link to={`${ROUTES.APPLICATION}/${text}${search}`}>
|
||||
<Name>{text}</Name>
|
||||
</Link>
|
||||
),
|
||||
});
|
||||
|
||||
const columns: ColumnsType<DataProps> = [
|
||||
{
|
||||
title: 'Application',
|
||||
dataIndex: 'serviceName',
|
||||
key: 'serviceName',
|
||||
render: (text: string): JSX.Element => (
|
||||
<Link to={`${ROUTES.APPLICATION}/${text}${search}`}>
|
||||
<Name>{text}</Name>
|
||||
</Link>
|
||||
),
|
||||
...getColumnSearchProps('serviceName'),
|
||||
},
|
||||
{
|
||||
title: 'P99 latency (in ms)',
|
||||
dataIndex: 'p99',
|
||||
key: 'p99',
|
||||
defaultSortOrder: 'descend',
|
||||
sorter: (a: DataProps, b: DataProps): number => a.p99 - b.p99,
|
||||
render: (value: number): string => (value / 1000000).toFixed(2),
|
||||
},
|
||||
|
@ -37,6 +37,14 @@ function MetricsBuilderFormula({
|
||||
style={{ marginBottom: '0.5rem' }}
|
||||
rows={2}
|
||||
/>
|
||||
<Input
|
||||
onChange={(event): void => {
|
||||
handleFormulaChange({ formulaIndex, legend: event.target.value });
|
||||
}}
|
||||
size="middle"
|
||||
defaultValue={formulaData.legend}
|
||||
addonBefore="Legend Format"
|
||||
/>
|
||||
</QueryHeader>
|
||||
);
|
||||
}
|
||||
|
@ -85,6 +85,7 @@ function QueryBuilderQueryContainer({
|
||||
const handleQueryBuilderFormulaChange = ({
|
||||
formulaIndex,
|
||||
expression,
|
||||
legend,
|
||||
toggleDisable,
|
||||
toggleDelete,
|
||||
}: IQueryBuilderFormulaHandleChange): void => {
|
||||
@ -94,9 +95,12 @@ function QueryBuilderQueryContainer({
|
||||
];
|
||||
const currentIndexFormula = allFormulas[formulaIndex as number];
|
||||
|
||||
if (expression) {
|
||||
if (expression !== undefined) {
|
||||
currentIndexFormula.expression = expression;
|
||||
}
|
||||
if (legend !== undefined) {
|
||||
currentIndexFormula.legend = legend;
|
||||
}
|
||||
|
||||
if (toggleDisable) {
|
||||
currentIndexFormula.disabled = !currentIndexFormula.disabled;
|
||||
|
@ -19,5 +19,6 @@ export interface IQueryBuilderFormulaHandleChange {
|
||||
formulaIndex: number | string;
|
||||
expression?: IMetricsBuilderFormula['expression'];
|
||||
toggleDisable?: IMetricsBuilderFormula['disabled'];
|
||||
legend?: IMetricsBuilderFormula['legend'];
|
||||
toggleDelete?: boolean;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
DashboardFilled,
|
||||
DeploymentUnitOutlined,
|
||||
LineChartOutlined,
|
||||
MenuOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import ROUTES from 'constants/routes';
|
||||
@ -18,10 +19,16 @@ const menus: SidebarMenu[] = [
|
||||
name: 'Services',
|
||||
},
|
||||
{
|
||||
Icon: AlignLeftOutlined,
|
||||
Icon: MenuOutlined,
|
||||
to: ROUTES.TRACE,
|
||||
name: 'Traces',
|
||||
},
|
||||
{
|
||||
Icon: AlignLeftOutlined,
|
||||
to: ROUTES.LOGS,
|
||||
name: 'Logs',
|
||||
tags: ['Beta'],
|
||||
},
|
||||
{
|
||||
Icon: DashboardFilled,
|
||||
to: ROUTES.ALL_DASHBOARD,
|
||||
@ -41,7 +48,6 @@ const menus: SidebarMenu[] = [
|
||||
to: ROUTES.SERVICE_MAP,
|
||||
name: 'Service Map',
|
||||
Icon: DeploymentUnitOutlined,
|
||||
tags: ['Beta'],
|
||||
},
|
||||
{
|
||||
Icon: LineChartOutlined,
|
||||
|
@ -18,6 +18,7 @@ const breadcrumbNameMap = {
|
||||
[ROUTES.ERROR_DETAIL]: 'Errors',
|
||||
[ROUTES.LIST_ALL_ALERT]: 'Alerts',
|
||||
[ROUTES.ALL_DASHBOARD]: 'Dashboard',
|
||||
[ROUTES.LOGS]: 'Logs',
|
||||
};
|
||||
|
||||
function ShowBreadcrumbs(props: RouteComponentProps): JSX.Element {
|
||||
|
@ -45,6 +45,10 @@ export const ServiceMapOptions: Option[] = [
|
||||
{ value: '5min', label: 'Last 5 min' },
|
||||
{ value: '15min', label: 'Last 15 min' },
|
||||
{ value: '30min', label: 'Last 30 min' },
|
||||
{ value: '1hr', label: 'Last 1 hour' },
|
||||
{ value: '6hr', label: 'Last 6 hour' },
|
||||
{ value: '1day', label: 'Last 1 day' },
|
||||
{ value: '1week', label: 'Last 1 week' },
|
||||
];
|
||||
|
||||
export const getDefaultOption = (route: string): Time => {
|
||||
|
209
frontend/src/lib/__fixtures__/logql.ts
Normal file
209
frontend/src/lib/__fixtures__/logql.ts
Normal file
@ -0,0 +1,209 @@
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
/* eslint no-useless-escape: 0 */
|
||||
|
||||
const logqlQueries = [
|
||||
{
|
||||
query: "OPERATION in ('bcd','xy\\'z') AND FULLTEXT contains 'helloxyz'", // Query with IN
|
||||
splitterQuery: [
|
||||
'OPERATION',
|
||||
'in',
|
||||
"('bcd','xy\\'z')",
|
||||
'AND',
|
||||
'FULLTEXT',
|
||||
'contains',
|
||||
"'helloxyz'",
|
||||
],
|
||||
parsedQuery: [
|
||||
{ type: 'QUERY_KEY', value: 'OPERATION' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'in' },
|
||||
{ type: 'QUERY_VALUE', value: ['bcd', "xy\\'z"] },
|
||||
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
|
||||
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'contains' },
|
||||
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
|
||||
],
|
||||
},
|
||||
{
|
||||
query: "OPERATION in ('bcd') and FULLTEXT contains 'helloxyz'", // Query with IN
|
||||
splitterQuery: [
|
||||
'OPERATION',
|
||||
'in',
|
||||
"('bcd')",
|
||||
'and',
|
||||
'FULLTEXT',
|
||||
'contains',
|
||||
"'helloxyz'",
|
||||
],
|
||||
parsedQuery: [
|
||||
{ type: 'QUERY_KEY', value: 'OPERATION' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'in' },
|
||||
{ type: 'QUERY_VALUE', value: ['bcd'] },
|
||||
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
|
||||
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'contains' },
|
||||
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
|
||||
],
|
||||
},
|
||||
{
|
||||
query: "OPERATION in ('bcd','xyz') AND FULLTEXT contains 'helloxyz'", // Query with IN
|
||||
splitterQuery: [
|
||||
'OPERATION',
|
||||
'in',
|
||||
"('bcd','xyz')",
|
||||
'AND',
|
||||
'FULLTEXT',
|
||||
'contains',
|
||||
"'helloxyz'",
|
||||
],
|
||||
parsedQuery: [
|
||||
{ type: 'QUERY_KEY', value: 'OPERATION' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'in' },
|
||||
{ type: 'QUERY_VALUE', value: ['bcd', 'xyz'] },
|
||||
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
|
||||
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'contains' },
|
||||
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
|
||||
],
|
||||
},
|
||||
{
|
||||
query: "status gte 200 AND FULLTEXT contains 'helloxyz'",
|
||||
splitterQuery: [
|
||||
'status',
|
||||
'gte',
|
||||
'200',
|
||||
'AND',
|
||||
'FULLTEXT',
|
||||
'contains',
|
||||
"'helloxyz'",
|
||||
],
|
||||
parsedQuery: [
|
||||
{ type: 'QUERY_KEY', value: 'status' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'gte' },
|
||||
{ type: 'QUERY_VALUE', value: '200' },
|
||||
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
|
||||
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'contains' },
|
||||
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
|
||||
],
|
||||
},
|
||||
{
|
||||
query: "service IN ('Hello (\\'World')", // Query with quotes and brackets
|
||||
splitterQuery: ['service', 'IN', "('Hello (\\'World')"],
|
||||
parsedQuery: [
|
||||
{ type: 'QUERY_KEY', value: 'service' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'IN' },
|
||||
{ type: 'QUERY_VALUE', value: ["Hello (\\'World"] },
|
||||
],
|
||||
},
|
||||
{
|
||||
query: "service IN ('Hello (\\'World') AND FULLTEXT contains 'ola'", // Query with full text as key pair
|
||||
splitterQuery: [
|
||||
'service',
|
||||
'IN',
|
||||
"('Hello (\\'World')",
|
||||
'AND',
|
||||
'FULLTEXT',
|
||||
'contains',
|
||||
"'ola'",
|
||||
],
|
||||
parsedQuery: [
|
||||
{ type: 'QUERY_KEY', value: 'service' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'IN' },
|
||||
{ type: 'QUERY_VALUE', value: ["Hello (\\'World"] },
|
||||
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
|
||||
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'contains' },
|
||||
{ type: 'QUERY_VALUE', value: "'ola'" },
|
||||
],
|
||||
},
|
||||
{
|
||||
query: 'id lt 100 and id gt 50 and code lte 500 and code gte 400', // Query with numbers
|
||||
splitterQuery: [
|
||||
'id',
|
||||
'lt',
|
||||
'100',
|
||||
'and',
|
||||
'id',
|
||||
'gt',
|
||||
'50',
|
||||
'and',
|
||||
'code',
|
||||
'lte',
|
||||
'500',
|
||||
'and',
|
||||
'code',
|
||||
'gte',
|
||||
'400',
|
||||
],
|
||||
parsedQuery: [
|
||||
{ type: 'QUERY_KEY', value: 'id' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'lt' },
|
||||
{ type: 'QUERY_VALUE', value: '100' },
|
||||
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
|
||||
{ type: 'QUERY_KEY', value: 'id' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'gt' },
|
||||
{ type: 'QUERY_VALUE', value: '50' },
|
||||
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
|
||||
{ type: 'QUERY_KEY', value: 'code' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'lte' },
|
||||
{ type: 'QUERY_VALUE', value: '500' },
|
||||
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
|
||||
{ type: 'QUERY_KEY', value: 'code' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'gte' },
|
||||
{ type: 'QUERY_VALUE', value: '400' },
|
||||
],
|
||||
},
|
||||
{
|
||||
query: 'FULLTEXT contains \'Hello, "World"\'', // Full text with Quotes
|
||||
splitterQuery: ['FULLTEXT', 'contains', `'Hello, "World"'`],
|
||||
parsedQuery: [
|
||||
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'contains' },
|
||||
{ type: 'QUERY_VALUE', value: `'Hello, "World"'` },
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
query: "service NIN ('name > 100') AND length gt 100", // Characters inside string
|
||||
splitterQuery: [
|
||||
'service',
|
||||
'NIN',
|
||||
"('name > 100')",
|
||||
'AND',
|
||||
'length',
|
||||
'gt',
|
||||
'100',
|
||||
],
|
||||
parsedQuery: [
|
||||
{ type: 'QUERY_KEY', value: 'service' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'NIN' },
|
||||
{ type: 'QUERY_VALUE', value: ['name > 100'] },
|
||||
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
|
||||
{ type: 'QUERY_KEY', value: 'length' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'gt' },
|
||||
{ type: 'QUERY_VALUE', value: '100' },
|
||||
],
|
||||
},
|
||||
|
||||
// Template
|
||||
{
|
||||
query: '',
|
||||
splitterQuery: [],
|
||||
parsedQuery: [],
|
||||
},
|
||||
];
|
||||
|
||||
const specialLogQLQueries = [
|
||||
{
|
||||
query: 'key IN 22', // Fulltext
|
||||
splitterQuery: ['key', 'IN', '22'],
|
||||
parsedQuery: [
|
||||
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
|
||||
{ type: 'QUERY_OPERATOR', value: 'CONTAINS' },
|
||||
{ type: 'QUERY_VALUE', value: 'key IN 22' },
|
||||
],
|
||||
reverseParsed: "FULLTEXT CONTAINS 'key IN 22'",
|
||||
},
|
||||
];
|
||||
|
||||
export { logqlQueries, specialLogQLQueries };
|
15
frontend/src/lib/__tests__/logql/parser.test.ts
Normal file
15
frontend/src/lib/__tests__/logql/parser.test.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { logqlQueries } from 'lib/__fixtures__/logql';
|
||||
import parser from 'lib/logql/parser';
|
||||
|
||||
describe('lib/logql/parser', () => {
|
||||
test('parse valid queries', () => {
|
||||
logqlQueries.forEach((queryObject) => {
|
||||
try {
|
||||
parser(queryObject.query);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
expect(parser(queryObject.query)).toEqual(queryObject.parsedQuery);
|
||||
});
|
||||
});
|
||||
});
|
14
frontend/src/lib/__tests__/logql/reverseParser.test.ts
Normal file
14
frontend/src/lib/__tests__/logql/reverseParser.test.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { logqlQueries } from 'lib/__fixtures__/logql';
|
||||
import { reverseParser } from 'lib/logql/reverseParser';
|
||||
|
||||
describe('lib/logql/reverseParser', () => {
|
||||
test('reverse parse valid queries', () => {
|
||||
logqlQueries.forEach((queryObject) => {
|
||||
try {
|
||||
expect(reverseParser(queryObject.parsedQuery)).toEqual(queryObject.query);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
10
frontend/src/lib/__tests__/logql/splitter.test.ts
Normal file
10
frontend/src/lib/__tests__/logql/splitter.test.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { logqlQueries } from 'lib/__fixtures__/logql';
|
||||
import { splitter } from 'lib/logql/splitter';
|
||||
|
||||
describe('lib/logql/splitter', () => {
|
||||
test('splitter valid quereies', () => {
|
||||
logqlQueries.forEach((queryObject) => {
|
||||
expect(splitter(queryObject.query)).toEqual(queryObject.splitterQuery);
|
||||
});
|
||||
});
|
||||
});
|
7
frontend/src/lib/logql/errors/ConvertToFullText.ts
Normal file
7
frontend/src/lib/logql/errors/ConvertToFullText.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export class ErrorConvertToFullText extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ErrorConvertToFullText';
|
||||
Object.setPrototypeOf(this, ErrorConvertToFullText.prototype);
|
||||
}
|
||||
}
|
7
frontend/src/lib/logql/errors/InvalidQueryPair.ts
Normal file
7
frontend/src/lib/logql/errors/InvalidQueryPair.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export class ErrorInvalidQueryPair extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'ErrorInvalidQueryPair';
|
||||
Object.setPrototypeOf(this, ErrorInvalidQueryPair.prototype);
|
||||
}
|
||||
}
|
2
frontend/src/lib/logql/errors/index.ts
Normal file
2
frontend/src/lib/logql/errors/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './ConvertToFullText';
|
||||
export * from './InvalidQueryPair';
|
4
frontend/src/lib/logql/index.ts
Normal file
4
frontend/src/lib/logql/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './parser';
|
||||
export * from './reverseParser';
|
||||
export * from './splitter';
|
||||
export * from './tokens';
|
152
frontend/src/lib/logql/parser.ts
Normal file
152
frontend/src/lib/logql/parser.ts
Normal file
@ -0,0 +1,152 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
// @ts-nocheck
|
||||
|
||||
import {
|
||||
ErrorConvertToFullText,
|
||||
ErrorInvalidQueryPair,
|
||||
} from 'lib/logql/errors';
|
||||
import splitter from 'lib/logql/splitter';
|
||||
import {
|
||||
ConditionalOperators,
|
||||
QueryOperatorsMultiVal,
|
||||
QueryOperatorsSingleVal,
|
||||
QueryTypes,
|
||||
} from 'lib/logql/tokens';
|
||||
|
||||
const validateMultiValue = (queryToken: string): boolean => {
|
||||
const queryValues = [];
|
||||
let start;
|
||||
let isQuoteStart = false;
|
||||
if (queryToken[0] === '(' && queryToken[queryToken.length - 1] === ')') {
|
||||
for (let idx = 1; idx < queryToken.length - 1; idx += 1) {
|
||||
if (queryToken[idx] === "'") {
|
||||
if (queryToken[idx - 1] === '\\') {
|
||||
// skip
|
||||
} else if (isQuoteStart) {
|
||||
isQuoteStart = false;
|
||||
queryValues.push(queryToken.slice(start, idx));
|
||||
} else {
|
||||
isQuoteStart = true;
|
||||
start = idx + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return queryValues;
|
||||
};
|
||||
export const parseQuery = (queryString) => {
|
||||
let parsedRaw = [];
|
||||
const generateQuery = (queryToken) => {
|
||||
const prevToken = parsedRaw[parsedRaw.length - 1];
|
||||
|
||||
// Is a QUERY_KEY
|
||||
if (
|
||||
prevToken === undefined ||
|
||||
prevToken.type === QueryTypes.CONDITIONAL_OPERATOR
|
||||
) {
|
||||
parsedRaw.push({
|
||||
type: QueryTypes.QUERY_KEY,
|
||||
value: queryToken,
|
||||
});
|
||||
}
|
||||
// Is a QUERY_OPERATOR
|
||||
else if (prevToken && prevToken.type === QueryTypes.QUERY_KEY) {
|
||||
if (
|
||||
Object.values({
|
||||
...QueryOperatorsMultiVal,
|
||||
...QueryOperatorsSingleVal,
|
||||
}).find((op) => op.toLowerCase() === queryToken.toLowerCase())
|
||||
)
|
||||
parsedRaw.push({
|
||||
type: QueryTypes.QUERY_OPERATOR,
|
||||
value: queryToken,
|
||||
});
|
||||
else {
|
||||
throw new ErrorInvalidQueryPair(
|
||||
'Expected conditional operator received',
|
||||
queryToken,
|
||||
);
|
||||
}
|
||||
}
|
||||
// Is a QUERY_VALUE
|
||||
else if (prevToken && prevToken.type === QueryTypes.QUERY_OPERATOR) {
|
||||
// Check for multi value
|
||||
let value = queryToken;
|
||||
// if (
|
||||
// typeof queryToken === 'string' &&
|
||||
// queryToken.length >= 2 &&
|
||||
// queryToken[0] === "'" &&
|
||||
// queryToken[queryToken.length - 1] === "'"
|
||||
// ) {
|
||||
// value = queryToken.slice(1, queryToken.length - 1);
|
||||
// }
|
||||
if (
|
||||
Object.values(QueryOperatorsMultiVal).some(
|
||||
(operator) => operator.toLowerCase() === prevToken.value.toLowerCase(),
|
||||
)
|
||||
) {
|
||||
value = validateMultiValue(queryToken);
|
||||
if (value === false) {
|
||||
throw new ErrorConvertToFullText();
|
||||
}
|
||||
}
|
||||
|
||||
parsedRaw.push({
|
||||
type: QueryTypes.QUERY_VALUE,
|
||||
value,
|
||||
});
|
||||
} else if (prevToken && prevToken.type === QueryTypes.QUERY_VALUE) {
|
||||
if (
|
||||
Object.values(ConditionalOperators).find(
|
||||
(op) => op.toLowerCase() === queryToken.toLowerCase(),
|
||||
)
|
||||
)
|
||||
parsedRaw.push({
|
||||
type: QueryTypes.CONDITIONAL_OPERATOR,
|
||||
value: queryToken,
|
||||
});
|
||||
else {
|
||||
throw new ErrorInvalidQueryPair(
|
||||
'Expected conditional operator received',
|
||||
queryToken,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Not a Key
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
const spaceSplittedQUery = splitter(queryString);
|
||||
spaceSplittedQUery.forEach((q) => {
|
||||
generateQuery(q);
|
||||
});
|
||||
} catch (e: Error) {
|
||||
if (e instanceof ErrorInvalidQueryPair) {
|
||||
//
|
||||
} else if (e instanceof ErrorConvertToFullText) {
|
||||
parsedRaw = [
|
||||
{
|
||||
type: QueryTypes.QUERY_KEY,
|
||||
value: 'FULLTEXT',
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_OPERATOR,
|
||||
value: 'CONTAINS',
|
||||
},
|
||||
{
|
||||
type: QueryTypes.QUERY_VALUE,
|
||||
value: String.raw`${queryString}`,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(parsedRaw);
|
||||
return parsedRaw;
|
||||
};
|
||||
|
||||
export default parseQuery;
|
25
frontend/src/lib/logql/reverseParser.ts
Normal file
25
frontend/src/lib/logql/reverseParser.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
// @ts-nocheck
|
||||
|
||||
export const reverseParser = (
|
||||
parserQueryArr: { type: string; value: any }[] = [],
|
||||
) => {
|
||||
let queryString = '';
|
||||
parserQueryArr.forEach((query) => {
|
||||
if (queryString) {
|
||||
queryString += ' ';
|
||||
}
|
||||
|
||||
if (Array.isArray(query.value) && query.value.length > 0) {
|
||||
queryString += `(${query.value.map((val) => `'${val}'`).join(',')})`;
|
||||
} else {
|
||||
queryString += query.value;
|
||||
}
|
||||
});
|
||||
|
||||
// console.log(queryString);
|
||||
return queryString;
|
||||
};
|
||||
|
||||
export default reverseParser;
|
52
frontend/src/lib/logql/splitter.ts
Normal file
52
frontend/src/lib/logql/splitter.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/* eslint-disable */
|
||||
// @ts-ignore
|
||||
// @ts-nocheck
|
||||
|
||||
export const splitter = (queryString: string): string[] => {
|
||||
const splittedParts: string[] = [];
|
||||
let start = 0;
|
||||
let isBracketStart = false;
|
||||
let isQuoteStart = false;
|
||||
|
||||
const pushPart = (idx) => {
|
||||
splittedParts.push(queryString.slice(start, idx));
|
||||
start = idx + 1;
|
||||
};
|
||||
for (let idx = 0; idx < queryString.length; idx += 1) {
|
||||
const currentChar = queryString[idx];
|
||||
|
||||
if (currentChar === ' ') {
|
||||
if (!isBracketStart && !isQuoteStart) {
|
||||
pushPart(idx);
|
||||
}
|
||||
} else if (currentChar === '(') {
|
||||
isBracketStart = true;
|
||||
} else if (currentChar === ')') {
|
||||
if (queryString[idx - 1] !== '\\') {
|
||||
pushPart(idx + 1);
|
||||
isBracketStart = false;
|
||||
}
|
||||
if (isQuoteStart) {
|
||||
isQuoteStart = false;
|
||||
}
|
||||
} else if (currentChar === "'") {
|
||||
if (isQuoteStart) {
|
||||
if (queryString[idx - 1] !== '\\' && !isBracketStart) {
|
||||
pushPart(idx + 1);
|
||||
isQuoteStart = false;
|
||||
}
|
||||
} else {
|
||||
isQuoteStart = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process remaining part
|
||||
if (start < queryString.length) {
|
||||
pushPart(queryString.length);
|
||||
}
|
||||
|
||||
return splittedParts.map((s) => String.raw`${s}`).filter(Boolean);
|
||||
};
|
||||
|
||||
export default splitter;
|
25
frontend/src/lib/logql/tokens.ts
Normal file
25
frontend/src/lib/logql/tokens.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export const QueryOperatorsSingleVal = {
|
||||
GTE: 'GTE',
|
||||
GT: 'GT',
|
||||
LTE: 'LTE',
|
||||
LT: 'LT',
|
||||
CONTAINS: 'CONTAINS',
|
||||
NCONTAINS: 'NCONTAINS',
|
||||
};
|
||||
|
||||
export const QueryOperatorsMultiVal = {
|
||||
IN: 'IN',
|
||||
NIN: 'NIN',
|
||||
};
|
||||
|
||||
export const ConditionalOperators = {
|
||||
AND: 'AND',
|
||||
OR: 'OR',
|
||||
};
|
||||
|
||||
export const QueryTypes = {
|
||||
QUERY_KEY: 'QUERY_KEY',
|
||||
QUERY_OPERATOR: 'QUERY_OPERATOR',
|
||||
QUERY_VALUE: 'QUERY_VALUE',
|
||||
CONDITIONAL_OPERATOR: 'CONDITIONAL_OPERATOR',
|
||||
};
|
4
frontend/src/lib/logql/types.ts
Normal file
4
frontend/src/lib/logql/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface ILogQLParsedQueryItem {
|
||||
type: string;
|
||||
value: string | string[];
|
||||
}
|
9
frontend/src/lib/logs/fieldSearch.ts
Normal file
9
frontend/src/lib/logs/fieldSearch.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export const fieldSearchFilter = (
|
||||
searchSpace = '',
|
||||
currentValue = '',
|
||||
): boolean => {
|
||||
if (!currentValue || !searchSpace) {
|
||||
return true;
|
||||
}
|
||||
return searchSpace.toLowerCase().indexOf(currentValue.toLowerCase()) !== -1;
|
||||
};
|
16
frontend/src/lib/logs/flatLogData.ts
Normal file
16
frontend/src/lib/logs/flatLogData.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
export function FlatLogData(log: ILog): Record<string, unknown> {
|
||||
const flattenLogObject: Record<string, unknown> = {};
|
||||
|
||||
Object.keys(log).forEach((key: string): void => {
|
||||
if (typeof log[key as never] !== 'object') {
|
||||
flattenLogObject[key] = log[key as never];
|
||||
} else {
|
||||
Object.keys(log[key as never]).forEach((childKey) => {
|
||||
flattenLogObject[childKey] = log[key as never][childKey];
|
||||
});
|
||||
}
|
||||
});
|
||||
return flattenLogObject;
|
||||
}
|
24
frontend/src/lib/logs/generateFilterQuery.ts
Normal file
24
frontend/src/lib/logs/generateFilterQuery.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { QueryOperatorsMultiVal } from 'lib/logql/tokens';
|
||||
|
||||
type Keys = keyof typeof QueryOperatorsMultiVal;
|
||||
type Values = typeof QueryOperatorsMultiVal[Keys];
|
||||
|
||||
interface GenerateFilterQueryParams {
|
||||
fieldKey: string;
|
||||
fieldValue: string;
|
||||
type: Values;
|
||||
}
|
||||
export const generateFilterQuery = ({
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
type,
|
||||
}: GenerateFilterQueryParams): string => {
|
||||
let generatedQueryString = `${fieldKey} ${type} `;
|
||||
if (typeof fieldValue === 'number') {
|
||||
generatedQueryString += `(${fieldValue})`;
|
||||
} else {
|
||||
generatedQueryString += `('${fieldValue}')`;
|
||||
}
|
||||
|
||||
return generatedQueryString;
|
||||
};
|
8
frontend/src/pages/Logs/index.tsx
Normal file
8
frontend/src/pages/Logs/index.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import Logs from 'container/Logs';
|
||||
import React from 'react';
|
||||
|
||||
function LogsHome(): JSX.Element {
|
||||
return <Logs />;
|
||||
}
|
||||
|
||||
export default LogsHome;
|
@ -76,9 +76,11 @@ export async function GetMetricQueryRange({
|
||||
|
||||
queryData[WIDGET_QUERY_BUILDER_FORMULA_KEY_NAME].map((formula) => {
|
||||
const generatedFormulaPayload = {};
|
||||
legendMap[formula.name] = formula.legend || formula.name;
|
||||
generatedFormulaPayload.queryName = formula.name;
|
||||
generatedFormulaPayload.expression = formula.expression;
|
||||
generatedFormulaPayload.disabled = formula.disabled;
|
||||
generatedFormulaPayload.legend = formula.legend;
|
||||
builderQueries[formula.name] = generatedFormulaPayload;
|
||||
});
|
||||
QueryPayload.compositeMetricQuery.builderQueries = builderQueries;
|
||||
|
17
frontend/src/store/actions/logs/addToSelectedField.ts
Normal file
17
frontend/src/store/actions/logs/addToSelectedField.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import GetSearchFields from 'api/logs/GetSearchFields';
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
import { SET_FIELDS } from 'types/actions/logs';
|
||||
|
||||
export const AddToSelectedField = (): ((
|
||||
dispatch: Dispatch<AppActions>,
|
||||
) => void) => {
|
||||
return async (dispatch): Promise<void> => {
|
||||
const response = await GetSearchFields();
|
||||
if (response.payload)
|
||||
dispatch({
|
||||
type: SET_FIELDS,
|
||||
payload: response.payload,
|
||||
});
|
||||
};
|
||||
};
|
23
frontend/src/store/actions/logs/getFields.ts
Normal file
23
frontend/src/store/actions/logs/getFields.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import GetSearchFields from 'api/logs/GetSearchFields';
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
import { SET_FIELDS } from 'types/actions/logs';
|
||||
|
||||
const IGNORED_SELECTED_FIELDS = ['timestamp'];
|
||||
|
||||
export const GetLogsFields = (): ((dispatch: Dispatch<AppActions>) => void) => {
|
||||
return async (dispatch): Promise<void> => {
|
||||
const response = await GetSearchFields();
|
||||
if (response.payload) {
|
||||
dispatch({
|
||||
type: SET_FIELDS,
|
||||
payload: {
|
||||
interesting: response.payload.interesting,
|
||||
selected: response.payload.selected.filter(
|
||||
(field) => !IGNORED_SELECTED_FIELDS.includes(field.name),
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
34
frontend/src/store/actions/logs/getLogs.ts
Normal file
34
frontend/src/store/actions/logs/getLogs.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import GetLogs from 'api/logs/GetLogs';
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
import { SET_LOADING, SET_LOGS } from 'types/actions/logs';
|
||||
import { Props } from 'types/api/logs/getLogs';
|
||||
|
||||
export const getLogs = (
|
||||
props: Props,
|
||||
): ((dispatch: Dispatch<AppActions>) => void) => {
|
||||
return async (dispatch): Promise<void> => {
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: true,
|
||||
});
|
||||
|
||||
const response = await GetLogs(props);
|
||||
|
||||
if (response.payload)
|
||||
dispatch({
|
||||
type: SET_LOGS,
|
||||
payload: response.payload,
|
||||
});
|
||||
else
|
||||
dispatch({
|
||||
type: SET_LOGS,
|
||||
payload: [],
|
||||
});
|
||||
|
||||
dispatch({
|
||||
type: SET_LOADING,
|
||||
payload: false,
|
||||
});
|
||||
};
|
||||
};
|
44
frontend/src/store/actions/logs/getLogsAggregate.ts
Normal file
44
frontend/src/store/actions/logs/getLogsAggregate.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import GetLogsAggregate from 'api/logs/GetLogsAggregate';
|
||||
import { Dispatch } from 'redux';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
SET_LOADING_AGGREGATE,
|
||||
SET_LOGS_AGGREGATE_SERIES,
|
||||
} from 'types/actions/logs';
|
||||
import { Props } from 'types/api/logs/getLogsAggregate';
|
||||
import { ILogsAggregate } from 'types/api/logs/logAggregate';
|
||||
|
||||
export const getLogsAggregate = (
|
||||
props: Props,
|
||||
): ((dispatch: Dispatch<AppActions>) => void) => {
|
||||
return async (dispatch): Promise<void> => {
|
||||
dispatch({
|
||||
type: SET_LOADING_AGGREGATE,
|
||||
payload: true,
|
||||
});
|
||||
|
||||
const response = await GetLogsAggregate(props);
|
||||
if (response.payload) {
|
||||
const convertedArray: ILogsAggregate[] = Object.values(response.payload).map(
|
||||
(data) => {
|
||||
return { ...data, time: new Date(data.timestamp / 1e6) };
|
||||
},
|
||||
);
|
||||
|
||||
dispatch({
|
||||
type: SET_LOGS_AGGREGATE_SERIES,
|
||||
payload: convertedArray,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
type: SET_LOGS_AGGREGATE_SERIES,
|
||||
payload: [],
|
||||
});
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: SET_LOADING_AGGREGATE,
|
||||
payload: false,
|
||||
});
|
||||
};
|
||||
};
|
@ -3,6 +3,7 @@ import { combineReducers } from 'redux';
|
||||
import appReducer from './app';
|
||||
import dashboardReducer from './dashboard';
|
||||
import globalTimeReducer from './global';
|
||||
import { LogsReducer } from './logs';
|
||||
import metricsReducers from './metric';
|
||||
import { ServiceMapReducer } from './serviceMap';
|
||||
import traceReducer from './trace';
|
||||
@ -16,6 +17,7 @@ const reducers = combineReducers({
|
||||
dashboards: dashboardReducer,
|
||||
app: appReducer,
|
||||
metrics: metricsReducers,
|
||||
logs: LogsReducer,
|
||||
});
|
||||
|
||||
export type AppState = ReturnType<typeof reducers>;
|
||||
|
213
frontend/src/store/reducers/logs.ts
Normal file
213
frontend/src/store/reducers/logs.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import { parseQuery } from 'lib/logql';
|
||||
import {
|
||||
ADD_SEARCH_FIELD_QUERY_STRING,
|
||||
FLUSH_LOGS,
|
||||
GET_FIELDS,
|
||||
GET_NEXT_LOG_LINES,
|
||||
GET_PREVIOUS_LOG_LINES,
|
||||
LogsActions,
|
||||
PUSH_LIVE_TAIL_EVENT,
|
||||
RESET_ID_START_AND_END,
|
||||
SET_DETAILED_LOG_DATA,
|
||||
SET_FIELDS,
|
||||
SET_LIVE_TAIL_START_TIME,
|
||||
SET_LOADING,
|
||||
SET_LOADING_AGGREGATE,
|
||||
SET_LOG_LINES_PER_PAGE,
|
||||
SET_LOGS,
|
||||
SET_LOGS_AGGREGATE_SERIES,
|
||||
SET_SEARCH_QUERY_PARSED_PAYLOAD,
|
||||
SET_SEARCH_QUERY_STRING,
|
||||
STOP_LIVE_TAIL,
|
||||
TOGGLE_LIVE_TAIL,
|
||||
} from 'types/actions/logs';
|
||||
import { ILogsReducer } from 'types/reducer/logs';
|
||||
|
||||
const initialState: ILogsReducer = {
|
||||
fields: {
|
||||
interesting: [],
|
||||
selected: [],
|
||||
},
|
||||
searchFilter: {
|
||||
queryString: '',
|
||||
parsedQuery: [],
|
||||
},
|
||||
logs: [],
|
||||
logLinesPerPage: 25,
|
||||
idEnd: '',
|
||||
idStart: '',
|
||||
isLoading: false,
|
||||
isLoadingAggregate: false,
|
||||
logsAggregate: [],
|
||||
liveTail: 'STOPPED',
|
||||
liveTailStartRange: 15,
|
||||
selectedLogId: null,
|
||||
detailedLog: null,
|
||||
};
|
||||
|
||||
export const LogsReducer = (
|
||||
state = initialState,
|
||||
action: LogsActions,
|
||||
): ILogsReducer => {
|
||||
switch (action.type) {
|
||||
case SET_LOADING: {
|
||||
return {
|
||||
...state,
|
||||
isLoading: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
case SET_LOADING_AGGREGATE: {
|
||||
return {
|
||||
...state,
|
||||
isLoadingAggregate: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
case GET_FIELDS:
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
|
||||
case SET_FIELDS: {
|
||||
const newFields = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
fields: newFields,
|
||||
};
|
||||
}
|
||||
|
||||
case SET_SEARCH_QUERY_STRING: {
|
||||
return {
|
||||
...state,
|
||||
searchFilter: {
|
||||
...state.searchFilter,
|
||||
queryString: action.payload,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case SET_SEARCH_QUERY_PARSED_PAYLOAD: {
|
||||
return {
|
||||
...state,
|
||||
searchFilter: {
|
||||
...state.searchFilter,
|
||||
parsedQuery: action.payload,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case ADD_SEARCH_FIELD_QUERY_STRING: {
|
||||
const updatedQueryString =
|
||||
state.searchFilter.queryString ||
|
||||
`${
|
||||
state.searchFilter.queryString && state.searchFilter.queryString.length > 0
|
||||
? ' and '
|
||||
: ''
|
||||
}${action.payload}`;
|
||||
|
||||
const updatedParsedQuery = parseQuery(updatedQueryString);
|
||||
return {
|
||||
...state,
|
||||
searchFilter: {
|
||||
...state.searchFilter,
|
||||
queryString: updatedQueryString,
|
||||
parsedQuery: updatedParsedQuery,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
case SET_LOGS: {
|
||||
const logsData = action.payload;
|
||||
return {
|
||||
...state,
|
||||
logs: logsData,
|
||||
};
|
||||
}
|
||||
case SET_LOG_LINES_PER_PAGE: {
|
||||
return {
|
||||
...state,
|
||||
logLinesPerPage: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
case GET_PREVIOUS_LOG_LINES: {
|
||||
const idStart = state.logs.length > 0 ? state.logs[0].id : '';
|
||||
return {
|
||||
...state,
|
||||
idStart,
|
||||
idEnd: '',
|
||||
};
|
||||
}
|
||||
|
||||
case GET_NEXT_LOG_LINES: {
|
||||
const idEnd =
|
||||
state.logs.length > 0 ? state.logs[state.logs.length - 1].id : '';
|
||||
return {
|
||||
...state,
|
||||
idStart: '',
|
||||
idEnd,
|
||||
};
|
||||
}
|
||||
|
||||
case RESET_ID_START_AND_END: {
|
||||
return {
|
||||
...state,
|
||||
idEnd: '',
|
||||
idStart: '',
|
||||
};
|
||||
}
|
||||
|
||||
case SET_LOGS_AGGREGATE_SERIES: {
|
||||
return {
|
||||
...state,
|
||||
logsAggregate: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
case SET_DETAILED_LOG_DATA: {
|
||||
return {
|
||||
...state,
|
||||
detailedLog: action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
case TOGGLE_LIVE_TAIL: {
|
||||
return {
|
||||
...state,
|
||||
liveTail: action.payload,
|
||||
};
|
||||
}
|
||||
case STOP_LIVE_TAIL: {
|
||||
return {
|
||||
...state,
|
||||
logs: [],
|
||||
liveTail: 'STOPPED',
|
||||
};
|
||||
}
|
||||
case PUSH_LIVE_TAIL_EVENT: {
|
||||
return {
|
||||
...state,
|
||||
logs: action.payload.concat(state.logs).slice(0, 100),
|
||||
};
|
||||
}
|
||||
case SET_LIVE_TAIL_START_TIME: {
|
||||
return {
|
||||
...state,
|
||||
liveTailStartRange: action.payload,
|
||||
};
|
||||
}
|
||||
case FLUSH_LOGS: {
|
||||
return {
|
||||
...state,
|
||||
logs: [],
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default LogsReducer;
|
@ -1,6 +1,7 @@
|
||||
import { AppAction } from './app';
|
||||
import { DashboardActions } from './dashboard';
|
||||
import { GlobalTimeAction } from './globalTime';
|
||||
import { LogsActions } from './logs';
|
||||
import { MetricsActions } from './metrics';
|
||||
import { TraceActions } from './trace';
|
||||
|
||||
@ -9,6 +10,7 @@ type AppActions =
|
||||
| AppAction
|
||||
| GlobalTimeAction
|
||||
| MetricsActions
|
||||
| TraceActions;
|
||||
| TraceActions
|
||||
| LogsActions;
|
||||
|
||||
export default AppActions;
|
||||
|
147
frontend/src/types/actions/logs.ts
Normal file
147
frontend/src/types/actions/logs.ts
Normal file
@ -0,0 +1,147 @@
|
||||
// import { DBOverView } from 'types/api/metrics/getDBOverview';
|
||||
// import { ExternalAverageDuration } from 'types/api/metrics/getExternalAverageDuration';
|
||||
// import { ExternalError } from 'types/api/metrics/getExternalError';
|
||||
// import { ExternalService } from 'types/api/metrics/getExternalService';
|
||||
// import { IResourceAttributeQuery } from 'container/MetricsApplication/ResourceAttributesFilter/types';
|
||||
// import { ServicesList } from 'types/api/metrics/getService';
|
||||
// import { ServiceOverview } from 'types/api/metrics/getServiceOverview';
|
||||
// import { TopEndPoints } from 'types/api/metrics/getTopEndPoints';
|
||||
|
||||
import { ILogQLParsedQueryItem } from 'lib/logql/types';
|
||||
import { IFieldMoveToSelected, IFields } from 'types/api/logs/fields';
|
||||
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { ILogsAggregate } from 'types/api/logs/logAggregate';
|
||||
|
||||
// export const GET_SERVICE_LIST_SUCCESS = 'GET_SERVICE_LIST_SUCCESS';
|
||||
// export const GET_SERVICE_LIST_LOADING_START = 'GET_SERVICE_LIST_LOADING_START';
|
||||
// export const GET_SERVICE_LIST_ERROR = 'GET_SERVICE_LIST_ERROR';
|
||||
// export const GET_INITIAL_APPLICATION_LOADING =
|
||||
// 'GET_INITIAL_APPLICATION_LOADING';
|
||||
// export const GET_INITIAL_APPLICATION_ERROR = 'GET_INITIAL_APPLICATION_ERROR';
|
||||
// export const GET_INTIAL_APPLICATION_DATA = 'GET_INTIAL_APPLICATION_DATA';
|
||||
// export const RESET_INITIAL_APPLICATION_DATA = 'RESET_INITIAL_APPLICATION_DATA';
|
||||
export const GET_FIELDS = 'LOGS_GET_FIELDS';
|
||||
export const SET_FIELDS = 'LOGS_SET_FIELDS';
|
||||
export const SET_SEARCH_QUERY_STRING = 'LOGS_SET_SEARCH_QUERY_STRING';
|
||||
export const SET_SEARCH_QUERY_PARSED_PAYLOAD =
|
||||
'LOGS_SET_SEARCH_QUERY_PARSED_PAYLOAD';
|
||||
export const ADD_SEARCH_FIELD_QUERY_STRING =
|
||||
'LOGS_ADD_SEARCH_FIELD_QUERY_STRING';
|
||||
export const ADD_TO_SELECTED_FIELD = 'LOGS_ADD_TO_SELECTED_FIELD';
|
||||
export const SET_LOGS = 'LOGS_SET_LOGS';
|
||||
export const SET_LOG_LINES_PER_PAGE = 'LOGS_SET_LOG_LINES_PER_PAGE';
|
||||
export const GET_NEXT_LOG_LINES = 'LOGS_GET_NEXT_LOG_LINES';
|
||||
export const GET_PREVIOUS_LOG_LINES = 'LOGS_GET_PREVIOUS_LOG_LINES';
|
||||
export const RESET_ID_START_AND_END = 'LOGS_RESET_ID_START_AND_END';
|
||||
export const SET_LOADING = 'LOGS_SET_LOADING';
|
||||
export const SET_LOADING_AGGREGATE = 'LOGS_SET_LOADING_AGGREGATE';
|
||||
export const SET_LOGS_AGGREGATE_SERIES = 'LOGS_SET_LOGS_AGGREGATE_SERIES';
|
||||
export const SET_DETAILED_LOG_DATA = 'LOGS_SET_DETAILED_LOG_DATA';
|
||||
export const TOGGLE_LIVE_TAIL = 'LOGS_TOGGLE_LIVE_TAIL';
|
||||
export const PUSH_LIVE_TAIL_EVENT = 'LOGS_PUSH_LIVE_TAIL_EVENT';
|
||||
export const STOP_LIVE_TAIL = 'LOGS_STOP_LIVE_TAIL';
|
||||
export const FLUSH_LOGS = 'LOGS_FLUSH_LOGS';
|
||||
export const SET_LIVE_TAIL_START_TIME = 'LOGS_SET_LIVE_TAIL_START_TIME';
|
||||
export interface GetFields {
|
||||
type: typeof GET_FIELDS;
|
||||
}
|
||||
|
||||
export interface SetFields {
|
||||
type: typeof SET_FIELDS;
|
||||
payload: IFields;
|
||||
}
|
||||
export interface SetSearchQueryString {
|
||||
type: typeof SET_SEARCH_QUERY_STRING;
|
||||
payload: string;
|
||||
}
|
||||
|
||||
export interface SetSearchQueryParsedPayload {
|
||||
type: typeof SET_SEARCH_QUERY_PARSED_PAYLOAD;
|
||||
payload: ILogQLParsedQueryItem[];
|
||||
}
|
||||
export interface AddSearchFieldQueryString {
|
||||
type: typeof ADD_SEARCH_FIELD_QUERY_STRING;
|
||||
payload: string;
|
||||
}
|
||||
export interface AddToSelectedField {
|
||||
type: typeof ADD_TO_SELECTED_FIELD;
|
||||
payload: IFieldMoveToSelected;
|
||||
}
|
||||
|
||||
export interface UpdateLogs {
|
||||
type: typeof SET_LOGS;
|
||||
payload: ILog[];
|
||||
}
|
||||
export interface SetLogsLinesPerPage {
|
||||
type: typeof SET_LOG_LINES_PER_PAGE;
|
||||
payload: number;
|
||||
}
|
||||
|
||||
export interface PreviousLogsLines {
|
||||
type: typeof GET_PREVIOUS_LOG_LINES;
|
||||
}
|
||||
export interface NextLogsLines {
|
||||
type: typeof GET_NEXT_LOG_LINES;
|
||||
}
|
||||
export interface ResetIdStartAndEnd {
|
||||
type: typeof RESET_ID_START_AND_END;
|
||||
}
|
||||
export interface SetLoading {
|
||||
type: typeof SET_LOADING;
|
||||
payload: boolean;
|
||||
}
|
||||
export interface SetLoadingAggregate {
|
||||
type: typeof SET_LOADING_AGGREGATE;
|
||||
payload: boolean;
|
||||
}
|
||||
|
||||
export interface SetLogsAggregateSeries {
|
||||
type: typeof SET_LOGS_AGGREGATE_SERIES;
|
||||
payload: ILogsAggregate[];
|
||||
}
|
||||
export interface SetDetailedLogData {
|
||||
type: typeof SET_DETAILED_LOG_DATA;
|
||||
payload: ILog;
|
||||
}
|
||||
|
||||
export interface ToggleLiveTail {
|
||||
type: typeof TOGGLE_LIVE_TAIL;
|
||||
payload: TLogsLiveTailState;
|
||||
}
|
||||
export interface PushLiveTailEvent {
|
||||
type: typeof PUSH_LIVE_TAIL_EVENT;
|
||||
payload: ILog[];
|
||||
}
|
||||
export interface StopLiveTail {
|
||||
type: typeof STOP_LIVE_TAIL;
|
||||
}
|
||||
export interface FlushLogs {
|
||||
type: typeof FLUSH_LOGS;
|
||||
}
|
||||
export interface SetLiveTailStartTime {
|
||||
type: typeof SET_LIVE_TAIL_START_TIME;
|
||||
payload: number;
|
||||
}
|
||||
|
||||
export type LogsActions =
|
||||
| GetFields
|
||||
| SetFields
|
||||
| SetSearchQueryString
|
||||
| SetSearchQueryParsedPayload
|
||||
| AddSearchFieldQueryString
|
||||
| AddToSelectedField
|
||||
| UpdateLogs
|
||||
| SetLogsLinesPerPage
|
||||
| PreviousLogsLines
|
||||
| NextLogsLines
|
||||
| ResetIdStartAndEnd
|
||||
| SetLoading
|
||||
| SetLoadingAggregate
|
||||
| SetLogsAggregateSeries
|
||||
| SetDetailedLogData
|
||||
| ToggleLiveTail
|
||||
| PushLiveTailEvent
|
||||
| StopLiveTail
|
||||
| FlushLogs
|
||||
| SetLiveTailStartTime;
|
@ -28,10 +28,9 @@ export interface IBuilderQueries {
|
||||
// for api calls
|
||||
export interface IBuilderQuery
|
||||
extends Omit<
|
||||
IMetricQuery,
|
||||
'aggregateOperator' | 'legend' | 'metricName' | 'tagFilters'
|
||||
>,
|
||||
Omit<IFormulaQuery, 'expression'> {
|
||||
IMetricQuery,
|
||||
'aggregateOperator' | 'legend' | 'metricName' | 'tagFilters'
|
||||
> {
|
||||
aggregateOperator: EAggregateOperator | undefined;
|
||||
disabled: boolean;
|
||||
name: string;
|
||||
|
@ -71,6 +71,7 @@ export interface IMetricsBuilderFormula {
|
||||
expression: string;
|
||||
disabled: boolean;
|
||||
name: string;
|
||||
legend: string;
|
||||
}
|
||||
export interface IMetricsBuilderQuery {
|
||||
aggregateOperator: EAggregateOperator;
|
||||
|
4
frontend/src/types/api/logs/addToSelectedFields.ts
Normal file
4
frontend/src/types/api/logs/addToSelectedFields.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { IFieldMoveToSelected } from './fields';
|
||||
|
||||
export type Props = IFieldMoveToSelected;
|
||||
export type PayloadProps = IFieldMoveToSelected;
|
16
frontend/src/types/api/logs/fields.ts
Normal file
16
frontend/src/types/api/logs/fields.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export interface IField {
|
||||
name: string;
|
||||
type: string;
|
||||
dataType: string;
|
||||
}
|
||||
|
||||
export interface IFields {
|
||||
selected: ISelectedFields[];
|
||||
interesting: IInterestingFields[];
|
||||
}
|
||||
export type ISelectedFields = IField;
|
||||
export type IInterestingFields = IField;
|
||||
|
||||
export type IFieldMoveToSelected = IField & {
|
||||
selected: boolean;
|
||||
};
|
13
frontend/src/types/api/logs/getLogs.ts
Normal file
13
frontend/src/types/api/logs/getLogs.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { ILog } from './log';
|
||||
|
||||
export type PayloadProps = ILog[];
|
||||
export type Props = {
|
||||
q: string;
|
||||
limit: number;
|
||||
orderBy: string;
|
||||
order: string;
|
||||
idGt?: string;
|
||||
idLt?: string;
|
||||
timestampStart?: number;
|
||||
timestampEnd?: number;
|
||||
};
|
15
frontend/src/types/api/logs/getLogsAggregate.ts
Normal file
15
frontend/src/types/api/logs/getLogsAggregate.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export type PayloadProps = Record<
|
||||
string,
|
||||
{
|
||||
timestamp: number;
|
||||
value: number;
|
||||
}
|
||||
>;
|
||||
export type Props = {
|
||||
timestampStart: number;
|
||||
timestampEnd: number;
|
||||
step: number;
|
||||
q?: string;
|
||||
idGt?: string;
|
||||
idLt?: string;
|
||||
};
|
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