2
.github/workflows/dependency-review.yml
vendored
@ -19,4 +19,4 @@ jobs:
|
|||||||
- name: 'Dependency Review'
|
- name: 'Dependency Review'
|
||||||
with:
|
with:
|
||||||
fail-on-severity: high
|
fail-on-severity: high
|
||||||
uses: actions/dependency-review-action@v2
|
uses: actions/dependency-review-action@v3
|
||||||
|
11
.github/workflows/e2e-k3s.yaml
vendored
@ -15,6 +15,11 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup golang
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: "1.21"
|
||||||
|
|
||||||
- name: Build query-service image
|
- name: Build query-service image
|
||||||
env:
|
env:
|
||||||
DEV_BUILD: 1
|
DEV_BUILD: 1
|
||||||
@ -65,9 +70,9 @@ jobs:
|
|||||||
- name: Kick off a sample-app workload
|
- name: Kick off a sample-app workload
|
||||||
run: |
|
run: |
|
||||||
# start the locust swarm
|
# start the locust swarm
|
||||||
kubectl -n sample-application run strzal --image=djbingham/curl \
|
kubectl --namespace sample-application run strzal --image=djbingham/curl \
|
||||||
--restart='OnFailure' -i --rm --command -- curl -X POST -F \
|
--restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \
|
||||||
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm
|
'user_count=6' -F 'spawn_rate=2' http://locust-master:8089/swarm
|
||||||
|
|
||||||
- name: Get short commit SHA, display tunnel URL and IP Address of the worker node
|
- name: Get short commit SHA, display tunnel URL and IP Address of the worker node
|
||||||
id: get-subdomain
|
id: get-subdomain
|
||||||
|
20
.github/workflows/push.yaml
vendored
@ -20,13 +20,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: "1.21"
|
go-version: "1.21"
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@ -64,13 +64,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: "1.21"
|
go-version: "1.21"
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@ -115,11 +115,11 @@ jobs:
|
|||||||
run: npm run lint
|
run: npm run lint
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
@ -164,11 +164,11 @@ jobs:
|
|||||||
run: npm run lint
|
run: npm run lint
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
2
.github/workflows/staging-deployment.yaml
vendored
@ -11,7 +11,7 @@ jobs:
|
|||||||
environment: staging
|
environment: staging
|
||||||
steps:
|
steps:
|
||||||
- name: Executing remote ssh commands using ssh key
|
- name: Executing remote ssh commands using ssh key
|
||||||
uses: appleboy/ssh-action@v0.1.8
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
env:
|
env:
|
||||||
GITHUB_BRANCH: develop
|
GITHUB_BRANCH: develop
|
||||||
GITHUB_SHA: ${{ github.sha }}
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
|
7
.github/workflows/testing-deployment.yaml
vendored
@ -11,7 +11,7 @@ jobs:
|
|||||||
if: ${{ github.event.label.name == 'testing-deploy' }}
|
if: ${{ github.event.label.name == 'testing-deploy' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Executing remote ssh commands using ssh key
|
- name: Executing remote ssh commands using ssh key
|
||||||
uses: appleboy/ssh-action@v0.1.8
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
env:
|
env:
|
||||||
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||||
GITHUB_SHA: ${{ github.sha }}
|
GITHUB_SHA: ${{ github.sha }}
|
||||||
@ -33,8 +33,11 @@ jobs:
|
|||||||
git add .
|
git add .
|
||||||
git stash push -m "stashed on $(date --iso-8601=seconds)"
|
git stash push -m "stashed on $(date --iso-8601=seconds)"
|
||||||
git fetch origin
|
git fetch origin
|
||||||
git checkout ${GITHUB_BRANCH}
|
git checkout develop
|
||||||
git pull
|
git pull
|
||||||
|
# This is added to include the scenerio when new commit in PR is force-pushed
|
||||||
|
git branch -D ${GITHUB_BRANCH}
|
||||||
|
git checkout --track origin/${GITHUB_BRANCH}
|
||||||
make build-ee-query-service-amd64
|
make build-ee-query-service-amd64
|
||||||
make build-frontend-amd64
|
make build-frontend-amd64
|
||||||
make run-signoz
|
make run-signoz
|
@ -108,7 +108,7 @@ We support [OpenTelemetry](https://opentelemetry.io) as the library which you ca
|
|||||||
|
|
||||||
- Java
|
- Java
|
||||||
- Python
|
- Python
|
||||||
- NodeJS
|
- Node.js
|
||||||
- Go
|
- Go
|
||||||
- PHP
|
- PHP
|
||||||
- .NET
|
- .NET
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
version: "3.9"
|
version: "3.9"
|
||||||
|
|
||||||
x-clickhouse-defaults: &clickhouse-defaults
|
x-clickhouse-defaults: &clickhouse-defaults
|
||||||
image: clickhouse/clickhouse-server:23.11.1-alpine
|
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||||
tty: true
|
tty: true
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
@ -146,7 +146,7 @@ services:
|
|||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:0.38.2
|
image: signoz/query-service:0.39.0
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"-config=/root/config/prometheus.yml",
|
"-config=/root/config/prometheus.yml",
|
||||||
@ -186,7 +186,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:0.38.2
|
image: signoz/frontend:0.39.0
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
@ -199,7 +199,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:0.88.11
|
image: signoz/signoz-otel-collector:0.88.12
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
@ -237,7 +237,7 @@ services:
|
|||||||
- query-service
|
- query-service
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:0.88.11
|
image: signoz/signoz-schema-migrator:0.88.12
|
||||||
deploy:
|
deploy:
|
||||||
restart_policy:
|
restart_policy:
|
||||||
condition: on-failure
|
condition: on-failure
|
||||||
|
@ -123,15 +123,7 @@ exporters:
|
|||||||
clickhouselogsexporter:
|
clickhouselogsexporter:
|
||||||
dsn: tcp://clickhouse:9000/
|
dsn: tcp://clickhouse:9000/
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||||
timeout: 5s
|
timeout: 10s
|
||||||
sending_queue:
|
|
||||||
queue_size: 100
|
|
||||||
retry_on_failure:
|
|
||||||
enabled: true
|
|
||||||
initial_interval: 5s
|
|
||||||
max_interval: 30s
|
|
||||||
max_elapsed_time: 300s
|
|
||||||
|
|
||||||
extensions:
|
extensions:
|
||||||
health_check:
|
health_check:
|
||||||
endpoint: 0.0.0.0:13133
|
endpoint: 0.0.0.0:13133
|
||||||
|
@ -19,7 +19,7 @@ services:
|
|||||||
- ZOO_AUTOPURGE_INTERVAL=1
|
- ZOO_AUTOPURGE_INTERVAL=1
|
||||||
|
|
||||||
clickhouse:
|
clickhouse:
|
||||||
image: clickhouse/clickhouse-server:23.7.3-alpine
|
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||||
container_name: signoz-clickhouse
|
container_name: signoz-clickhouse
|
||||||
# ports:
|
# ports:
|
||||||
# - "9000:9000"
|
# - "9000:9000"
|
||||||
@ -66,7 +66,7 @@ services:
|
|||||||
- --storage.path=/data
|
- --storage.path=/data
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.11}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@ -81,7 +81,7 @@ services:
|
|||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
otel-collector:
|
otel-collector:
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
image: signoz/signoz-otel-collector:0.88.11
|
image: signoz/signoz-otel-collector:0.88.12
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
"--config=/etc/otel-collector-config.yaml",
|
"--config=/etc/otel-collector-config.yaml",
|
||||||
|
@ -3,7 +3,7 @@ version: "2.4"
|
|||||||
x-clickhouse-defaults: &clickhouse-defaults
|
x-clickhouse-defaults: &clickhouse-defaults
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
|
# addding non LTS version due to this fix https://github.com/ClickHouse/ClickHouse/commit/32caf8716352f45c1b617274c7508c86b7d1afab
|
||||||
image: clickhouse/clickhouse-server:23.11.1-alpine
|
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||||
tty: true
|
tty: true
|
||||||
depends_on:
|
depends_on:
|
||||||
- zookeeper-1
|
- zookeeper-1
|
||||||
@ -164,7 +164,7 @@ services:
|
|||||||
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
# Notes for Maintainers/Contributors who will change Line Numbers of Frontend & Query-Section. Please Update Line Numbers in `./scripts/commentLinesForSetup.sh` & `./CONTRIBUTING.md`
|
||||||
|
|
||||||
query-service:
|
query-service:
|
||||||
image: signoz/query-service:${DOCKER_TAG:-0.38.2}
|
image: signoz/query-service:${DOCKER_TAG:-0.39.0}
|
||||||
container_name: signoz-query-service
|
container_name: signoz-query-service
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
@ -203,7 +203,7 @@ services:
|
|||||||
<<: *db-depend
|
<<: *db-depend
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image: signoz/frontend:${DOCKER_TAG:-0.38.2}
|
image: signoz/frontend:${DOCKER_TAG:-0.39.0}
|
||||||
container_name: signoz-frontend
|
container_name: signoz-frontend
|
||||||
restart: on-failure
|
restart: on-failure
|
||||||
depends_on:
|
depends_on:
|
||||||
@ -215,7 +215,7 @@ services:
|
|||||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
otel-collector-migrator:
|
otel-collector-migrator:
|
||||||
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.11}
|
image: signoz/signoz-schema-migrator:${OTELCOL_TAG:-0.88.12}
|
||||||
container_name: otel-migrator
|
container_name: otel-migrator
|
||||||
command:
|
command:
|
||||||
- "--dsn=tcp://clickhouse:9000"
|
- "--dsn=tcp://clickhouse:9000"
|
||||||
@ -229,7 +229,7 @@ services:
|
|||||||
|
|
||||||
|
|
||||||
otel-collector:
|
otel-collector:
|
||||||
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.11}
|
image: signoz/signoz-otel-collector:${OTELCOL_TAG:-0.88.12}
|
||||||
container_name: signoz-otel-collector
|
container_name: signoz-otel-collector
|
||||||
command:
|
command:
|
||||||
[
|
[
|
||||||
|
@ -135,14 +135,7 @@ exporters:
|
|||||||
clickhouselogsexporter:
|
clickhouselogsexporter:
|
||||||
dsn: tcp://clickhouse:9000/
|
dsn: tcp://clickhouse:9000/
|
||||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||||
timeout: 5s
|
timeout: 10s
|
||||||
sending_queue:
|
|
||||||
queue_size: 100
|
|
||||||
retry_on_failure:
|
|
||||||
enabled: true
|
|
||||||
initial_interval: 5s
|
|
||||||
max_interval: 30s
|
|
||||||
max_elapsed_time: 300s
|
|
||||||
|
|
||||||
service:
|
service:
|
||||||
telemetry:
|
telemetry:
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# use a minimal alpine image
|
# use a minimal alpine image
|
||||||
FROM alpine:3.18.5
|
FROM alpine:3.18.6
|
||||||
|
|
||||||
# Add Maintainer Info
|
# Add Maintainer Info
|
||||||
LABEL maintainer="signoz"
|
LABEL maintainer="signoz"
|
||||||
|
@ -139,17 +139,13 @@ func (lm *Manager) UploadUsage() {
|
|||||||
|
|
||||||
zap.S().Info("uploading usage data")
|
zap.S().Info("uploading usage data")
|
||||||
|
|
||||||
// Try to get the org name
|
|
||||||
orgName := ""
|
orgName := ""
|
||||||
orgNames, err := lm.modelDao.GetOrgs(ctx)
|
orgNames, orgError := lm.modelDao.GetOrgs(ctx)
|
||||||
if err != nil {
|
if orgError != nil {
|
||||||
zap.S().Errorf("failed to get org data: %v", zap.Error(err))
|
zap.S().Errorf("failed to get org data: %v", zap.Error(orgError))
|
||||||
} else {
|
}
|
||||||
if len(orgNames) != 1 {
|
if len(orgNames) == 1 {
|
||||||
zap.S().Errorf("expected one org but got %d orgs", len(orgNames))
|
orgName = orgNames[0].Name
|
||||||
} else {
|
|
||||||
orgName = orgNames[0].Name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
usagesPayload := []model.Usage{}
|
usagesPayload := []model.Usage{}
|
||||||
|
@ -22,7 +22,7 @@ const config: Config.InitialOptions = {
|
|||||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: [
|
transformIgnorePatterns: [
|
||||||
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios)/)',
|
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens)/)',
|
||||||
],
|
],
|
||||||
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
setupFilesAfterEnv: ['<rootDir>jest.setup.ts'],
|
||||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||||
|
@ -36,7 +36,9 @@
|
|||||||
"@mdx-js/loader": "2.3.0",
|
"@mdx-js/loader": "2.3.0",
|
||||||
"@mdx-js/react": "2.3.0",
|
"@mdx-js/react": "2.3.0",
|
||||||
"@monaco-editor/react": "^4.3.1",
|
"@monaco-editor/react": "^4.3.1",
|
||||||
"@signozhq/design-tokens": "0.0.6",
|
"@radix-ui/react-tabs": "1.0.4",
|
||||||
|
"@radix-ui/react-tooltip": "1.0.7",
|
||||||
|
"@signozhq/design-tokens": "0.0.8",
|
||||||
"@uiw/react-md-editor": "3.23.5",
|
"@uiw/react-md-editor": "3.23.5",
|
||||||
"@xstate/react": "^3.0.0",
|
"@xstate/react": "^3.0.0",
|
||||||
"ansi-to-html": "0.7.2",
|
"ansi-to-html": "0.7.2",
|
||||||
|
1
frontend/public/Icons/awwSnap.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="32" height="33" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15.91 28.675c-6.199 0-12.888-3.888-12.888-12.421S9.711 3.832 15.911 3.832c3.444 0 6.621 1.134 8.977 3.2 2.555 2.267 3.91 5.466 3.91 9.222 0 3.755-1.355 6.933-3.91 9.2-2.356 2.066-5.555 3.221-8.977 3.221z" fill="url(#prefix__paint0_radial_2122_6520)"/><path d="M26.552 8.87c1.185 1.91 1.803 4.186 1.803 6.717 0 3.756-1.356 6.933-3.911 9.2-2.356 2.066-5.556 3.222-8.978 3.222-4.013 0-8.221-1.634-10.706-5.098 2.391 3.924 6.889 5.764 11.15 5.764 3.423 0 6.623-1.155 8.978-3.222 2.555-2.266 3.911-5.444 3.911-9.2 0-2.83-.771-5.346-2.247-7.383z" fill="#EB8F00"/><path d="M20.123 22.905c0 1.685-1.846 2.667-4.124 2.667-2.277 0-4.124-.989-4.124-2.667 0-1.677 1.847-3.522 4.124-3.522 2.278 0 4.124 1.838 4.124 3.522zM12.06 14.852l1.88-1.748c.267-.331.307-.778.038-1.045-.353-.355-.98-.269-1.32.136-.018.033-.03.042-.049.075l-1.333 1.938-1.804-1.682c-.027-.03-.042-.034-.067-.062-.42-.32-1.05-.267-1.315.157-.207.32-.07.745.264 1.011l2.313 1.372-1.96 1.833c-.262.326-.31.77-.04 1.044.351.358.978.276 1.32-.127.018-.033.031-.042.051-.075l1.405-2.031 1.706 1.609c.027.029.043.035.067.064.418.322 1.049.273 1.318-.149.206-.32.07-.746-.26-1.013l-2.213-1.307zM20.61 14.852l-1.879-1.748c-.267-.331-.307-.778-.036-1.045.354-.355.978-.269 1.318.136.018.033.034.042.051.075l1.334 1.938 1.806-1.682c.025-.03.04-.034.065-.062.422-.32 1.05-.267 1.317.157.205.32.067.745-.266 1.011L22 15.004l1.96 1.833c.268.33.313.775.042 1.044-.349.358-.976.276-1.318-.127-.02-.033-.033-.042-.051-.075l-1.404-2.031-1.71 1.609c-.024.029-.04.035-.066.064-.418.322-1.046.273-1.315-.149-.21-.32-.074-.746.257-1.013l2.216-1.307zM11.911 8.696c.511.044.711-.645.178-.8a4.07 4.07 0 00-1.289-.133A4.596 4.596 0 007.689 9.14c-.378.4.156.89.556.6a5.829 5.829 0 013.666-1.044zM20.044 8.696a5.85 5.85 0 013.689 1.044c.4.29.933-.2.555-.6a4.645 4.645 0 00-3.11-1.377 4.07 4.07 0 00-1.29.133.408.408 0 00-.282.504c.053.194.24.318.438.296z" fill="#422B0D"/><defs><radialGradient id="prefix__paint0_radial_2122_6520" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(15.91 16.254) scale(12.657)"><stop offset=".5" stop-color="#FDE030"/><stop offset=".92" stop-color="#F7C02B"/><stop offset="1" stop-color="#F4A223"/></radialGradient></defs></svg>
|
After Width: | Height: | Size: 2.3 KiB |
1
frontend/public/Icons/emptyState.svg
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
frontend/public/Icons/loading-plane.gif
Normal file
After Width: | Height: | Size: 88 KiB |
1
frontend/public/Icons/promQL.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c1 2.538 2.5 2.962 3.5 3.808.942.78 1.481 1.845 1.5 2.961 0 1.122-.527 2.198-1.464 2.992C14.598 12.554 13.326 13 12 13s-2.598-.446-3.536-1.24C7.527 10.968 7 9.892 7 8.77c0-.255 0-.508.1-.762.085.25.236.48.443.673.207.193.463.342.75.437a2.334 2.334 0 001.767-.128c.263-.135.485-.32.65-.539.166-.22.269-.468.301-.727a1.452 1.452 0 00-.11-.765 1.699 1.699 0 00-.501-.644C8 4.115 11 2 12 2zM17 16l-5 6-5-6h10z" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
After Width: | Height: | Size: 581 B |
1
frontend/public/Icons/tetra-pack.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg width="32" height="33" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.309 13.108l-6.704-3.32s-.016-.317.284-.477c.302-.16 5.053-2.107 5.435-2.107.383 0 2.62.431 4.249.793 1.629.363 5.933 1.287 5.953 1.57.02.281-4.404 4.806-4.404 4.806l-4.813-1.265z" fill="#C3FECE"/><path d="M20.423 11.037s-2.811-.826-5.546-1.469c-1.274-.3-5.016-1.084-5.016-1.084s.398-.173.698-.3c.305-.127.547-.193.547-.193s2.44.486 4.253.873c2.453.522 5.886 1.547 5.966 1.709.082.16-.902.464-.902.464z" fill="#fff"/><path d="M14.98 10.26c-.598.415-.011.666 1.09.924 1.207.282 2.127.698 2.903.247.7-.405-1.014-.845-1.8-1.014-.6-.129-1.731-.478-2.193-.158z" fill="#ACB1B2"/><path d="M17.17 11.095c-.005 0 .02-4.869.02-5.049 0-.18-.203-.342.02-.724.222-.382.804-.342.804-.342s2.416-.702 3.38-.945c.964-.242 3.098-.804 3.098-.804l.142 1.22s-2.236.631-3.342.913c-1.107.282-2.616.745-2.616.745l-.222.202.064 4.757s-.206.231-.668.231c-.45-.002-.68-.204-.68-.204z" fill="#FFD816"/><path d="M24.095 3.855c.018.38.22.616.46.616.24 0 .404-.307.369-.707-.038-.398-.296-.58-.516-.506-.22.073-.327.32-.313.597zM18.46 6.422a.209.209 0 01-.123-.038l-1.153-.769a.225.225 0 01-.063-.309.222.222 0 01.31-.062l1.153.769a.224.224 0 01.062.309.228.228 0 01-.187.1z" fill="#FEB804"/><path d="M18.636 6.235a.225.225 0 01-.178-.089c-.295-.393-.633-.84-.693-.909a.225.225 0 01-.031-.284.222.222 0 01.309-.062c.04.027.062.042.771.986.073.098.007.238-.091.312-.04.03-.04.046-.087.046z" fill="#FEB804"/><path d="M18.365 6.609c-.01 0-.022 0-.035-.003l-1.111-.175a.221.221 0 11.069-.438l1.11.176c.12.02.225.042.205.164-.016.107-.129.276-.238.276z" fill="#FEB804"/><path d="M7.596 9.764c.353 0 3.188.744 4.65 1.013 1.463.27 5.878 1.314 6.027 1.342.149.03.12 1.94.12 1.94s2.089 10.8 2.029 11.309c-.06.506-1.431 4.415-1.431 4.415s-.807.12-2.865-.478c-2.057-.598-7.488-2.089-7.817-2.506-.329-.418-.12-5.938-.298-9.338-.182-3.402-.415-7.697-.415-7.697z" fill="#79DD8A"/><path d="M24.06 27.036c.113-.375-.518-4.402-.607-8.101-.089-3.698.229-9.324.076-9.369-.154-.042-5.256 2.553-5.256 2.553s-.022 3.671.04 7.133c.08 4.48.438 10.41.676 10.53.238.12 2.302-1.035 2.924-1.372 1.102-.598 2.058-1.074 2.147-1.374z" fill="#02AB46"/><path d="M20.408 13.82l.011-2.787.914-.45.026 3.056-.422.74-.529-.56z" fill="#DBDFE1"/><path d="M12.322 14.797c-1.973-.211-3.34 1.549-3.233 3.842.127 2.709 1.91 4.704 3.842 5.102 1.93.398 3.802-.44 3.842-3.402.044-3.087-2.669-5.353-4.451-5.542z" fill="#FEFEFD"/><path d="M13.637 17.27s-.4-1.344-1.602-.986c-1.202.357-1.853 2.973.187 4.15 1.96 1.131 3.764-.944 3.133-2.288-.574-1.227-1.718-.876-1.718-.876z" fill="#EF5B44"/><path d="M13.18 15.626c-.136.049-.243.602-.1 1.13.106.396.446.939.643.903.158-.029.278-.651.13-1.173-.174-.602-.516-.918-.674-.86z" fill="#B8CF17"/><path d="M13.15 18.746c-.564-.171-1.2 1.769-.057 2.977 1.26 1.331 2.73.158 2.69-.1-.057-.358-1.044-.615-1.53-1.215-.487-.605-.774-1.562-1.102-1.662z" fill="#FD8F01"/><path d="M11.346 18.417s.113-.849-.673-.802c-.76.046-.574.944-.574.944s-.633.076-.526.778c.08.53.64.524.64.524s-.616.242-.336.945c.249.624.822.373.822.373s-.21.609.287.93c.42.272.787.043.787.043s-.023.52.557.616c.703.115 1.007-.74.507-1.136-.38-.3-.724-.067-.724-.067s.07-.166.004-.357c-.045-.125-.116-.171-.116-.171s.616-.058.516-.758c-.1-.702-.716-.616-.716-.616s.358-.286.216-.802c-.14-.518-.671-.444-.671-.444z" fill="#A281D0"/><path d="M21.04 14.595c-.511 0-2.691-2.167-2.711-2.189a.222.222 0 01.024-.313.224.224 0 01.314.022c.14.155 1.806 1.702 2.286 2 .311-.465 1.322-2.498 2.191-4.333a.224.224 0 01.296-.107.223.223 0 01.106.296c-2.142 4.526-2.353 4.586-2.466 4.617-.013.007-.027.007-.04.007z" fill="#2D802D"/></svg>
|
After Width: | Height: | Size: 3.6 KiB |
19
frontend/public/Images/eyesEmoji.svg
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M9.36806 25.9481C5.93935 25.9481 3.15283 21.7098 3.15283 16.5002C3.15283 11.2907 5.94157 7.05238 9.36806 7.05238C12.7945 7.05238 15.5833 11.2907 15.5833 16.5002C15.5833 21.7098 12.7945 25.9481 9.36806 25.9481Z" fill="#FAFAFA"/>
|
||||||
|
<path d="M9.36815 7.49694C10.8414 7.49694 12.2524 8.38594 13.3391 10.0017C14.499 11.7241 15.139 14.0333 15.139 16.5003C15.139 18.9673 14.499 21.2764 13.3391 22.9989C12.2524 24.6146 10.8414 25.5036 9.36815 25.5036C7.89489 25.5036 6.48385 24.6146 5.39724 22.9989C4.23508 21.2764 3.59734 18.9673 3.59734 16.5003C3.59734 14.0333 4.23731 11.7241 5.39724 10.0017C6.48385 8.38594 7.89267 7.49694 9.36815 7.49694ZM9.36815 6.60794C5.69056 6.60794 2.7085 11.0374 2.7085 16.5003C2.7085 21.9632 5.69056 26.3926 9.36815 26.3926C13.0457 26.3926 16.0278 21.9632 16.0278 16.5003C16.0278 11.0374 13.0457 6.60794 9.36815 6.60794Z" fill="#B0BEC5"/>
|
||||||
|
<path d="M7.47266 15.5762C6.87269 15.0118 7.00602 13.8919 7.77487 13.0741C7.81486 13.0319 7.85486 12.9919 7.89708 12.9541C7.55488 12.7608 7.17934 12.6519 6.78381 12.6519C5.18611 12.6519 3.89062 14.414 3.89062 16.585C3.89062 18.756 5.18611 20.5182 6.78381 20.5182C8.3815 20.5182 9.67699 18.756 9.67699 16.585C9.67699 16.1962 9.63477 15.8184 9.55699 15.4629C8.83703 15.9806 7.97708 16.0495 7.47266 15.5762Z" fill="url(#paint0_linear_2122_5062)"/>
|
||||||
|
<path d="M22.6294 26.3932C26.3074 26.3932 29.289 21.9642 29.289 16.5008C29.289 11.0374 26.3074 6.60847 22.6294 6.60847C18.9514 6.60847 15.9697 11.0374 15.9697 16.5008C15.9697 21.9642 18.9514 26.3932 22.6294 26.3932Z" fill="#EEEEEE"/>
|
||||||
|
<path d="M22.6283 25.9493C19.2018 25.9493 16.4131 21.711 16.4131 16.5014C16.4131 11.2919 19.2018 7.05357 22.6283 7.05357C26.0548 7.05357 28.8435 11.2919 28.8435 16.5014C28.8435 21.711 26.057 25.9493 22.6283 25.9493Z" fill="#FAFAFA"/>
|
||||||
|
<path d="M22.6284 7.49816C24.1017 7.49816 25.5127 8.38716 26.5993 10.0029C27.7592 11.7254 28.3992 14.0345 28.3992 16.5015C28.3992 18.9685 27.7592 21.2777 26.5993 23.0001C25.5127 24.6159 24.1017 25.5049 22.6284 25.5049C21.1551 25.5049 19.7441 24.6159 18.6575 23.0001C17.4976 21.2777 16.8576 18.9685 16.8576 16.5015C16.8576 14.0345 17.4976 11.7254 18.6575 10.0029C19.7441 8.38716 21.1551 7.49816 22.6284 7.49816ZM22.6284 6.60916C18.9508 6.60916 15.9688 11.0386 15.9688 16.5015C15.9688 21.9644 18.9508 26.3939 22.6284 26.3939C26.306 26.3939 29.2881 21.9644 29.2881 16.5015C29.2881 11.0386 26.306 6.60916 22.6284 6.60916Z" fill="#B0BEC5"/>
|
||||||
|
<path d="M20.7339 15.5767C20.1339 15.0123 20.2672 13.8924 21.0361 13.0746C21.0761 13.0324 21.1161 12.9924 21.1583 12.9546C20.8161 12.7613 20.4406 12.6524 20.045 12.6524C18.4473 12.6524 17.1519 14.4146 17.1519 16.5856C17.1519 18.7566 18.4473 20.5187 20.045 20.5187C21.6427 20.5187 22.9382 18.7566 22.9382 16.5856C22.9382 16.1967 22.896 15.8189 22.8182 15.4634C22.1005 15.9812 21.2383 16.05 20.7339 15.5767Z" fill="url(#paint1_linear_2122_5062)"/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_2122_5062" x1="6.78232" y1="12.651" x2="6.78232" y2="20.5188" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#424242"/>
|
||||||
|
<stop offset="1" stop-color="#212121"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="paint1_linear_2122_5062" x1="20.0449" y1="12.6515" x2="20.0449" y2="20.5193" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#424242"/>
|
||||||
|
<stop offset="1" stop-color="#212121"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 16 KiB |
BIN
frontend/public/Logos/swift.png
Normal file
After Width: | Height: | Size: 13 KiB |
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name_of_the_view": "Name of the view"
|
"name_of_the_view": "Name of the view",
|
||||||
|
"delete_confirm_message": "Are you sure you want to delete {{viewName}} view? Deleting a view is irreversible and cannot be undone."
|
||||||
}
|
}
|
@ -39,5 +39,8 @@
|
|||||||
"LIST_LICENSES": "SigNoz | List of Licenses",
|
"LIST_LICENSES": "SigNoz | List of Licenses",
|
||||||
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
||||||
"SUPPORT": "SigNoz | Support",
|
"SUPPORT": "SigNoz | Support",
|
||||||
"DEFAULT": "Open source Observability Platform | SigNoz"
|
"LOGS_SAVE_VIEWS": "SigNoz | Logs Save Views",
|
||||||
|
"TRACES_SAVE_VIEWS": "SigNoz | Traces Save Views",
|
||||||
|
"DEFAULT": "Open source Observability Platform | SigNoz",
|
||||||
|
"SHORTCUTS": "SigNoz | Shortcuts"
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ done
|
|||||||
# create temporary tsconfig which includes only passed files
|
# create temporary tsconfig which includes only passed files
|
||||||
str="{
|
str="{
|
||||||
\"extends\": \"./tsconfig.json\",
|
\"extends\": \"./tsconfig.json\",
|
||||||
\"include\": [\"src/types/global.d.ts\",\"src/typings/window.ts\", $files]
|
\"include\": [\"src/types/global.d.ts\",\"src/typings/window.ts\", \"src/typings/chartjs-adapter-date-fns.d.ts\", \"src/typings/environment.ts\" ,$files]
|
||||||
}"
|
}"
|
||||||
echo $str > tsconfig.tmp
|
echo $str > tsconfig.tmp
|
||||||
|
|
||||||
@ -22,4 +22,4 @@ code=$?
|
|||||||
# delete temp config
|
# delete temp config
|
||||||
rm ./tsconfig.tmp
|
rm ./tsconfig.tmp
|
||||||
|
|
||||||
exit $code
|
exit $code
|
||||||
|
@ -160,7 +160,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
|||||||
if (currentRoute) {
|
if (currentRoute) {
|
||||||
const { isPrivate, key } = currentRoute;
|
const { isPrivate, key } = currentRoute;
|
||||||
|
|
||||||
if (isPrivate && key !== ROUTES.WORKSPACE_LOCKED) {
|
if (isPrivate && key !== String(ROUTES.WORKSPACE_LOCKED)) {
|
||||||
handlePrivateRoutes(key);
|
handlePrivateRoutes(key);
|
||||||
} else {
|
} else {
|
||||||
// no need to fetch the user and make user fetching false
|
// no need to fetch the user and make user fetching false
|
||||||
|
@ -8,6 +8,7 @@ import { LOCALSTORAGE } from 'constants/localStorage';
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import AppLayout from 'container/AppLayout';
|
import AppLayout from 'container/AppLayout';
|
||||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||||
|
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||||
import { useThemeConfig } from 'hooks/useDarkMode';
|
import { useThemeConfig } from 'hooks/useDarkMode';
|
||||||
import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
|
import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
|
||||||
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
|
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
|
||||||
@ -177,22 +178,24 @@ function App(): JSX.Element {
|
|||||||
<ResourceProvider>
|
<ResourceProvider>
|
||||||
<QueryBuilderProvider>
|
<QueryBuilderProvider>
|
||||||
<DashboardProvider>
|
<DashboardProvider>
|
||||||
<AppLayout>
|
<KeyboardHotkeysProvider>
|
||||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
<AppLayout>
|
||||||
<Switch>
|
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||||
{routes.map(({ path, component, exact }) => (
|
<Switch>
|
||||||
<Route
|
{routes.map(({ path, component, exact }) => (
|
||||||
key={`${path}`}
|
<Route
|
||||||
exact={exact}
|
key={`${path}`}
|
||||||
path={path}
|
exact={exact}
|
||||||
component={component}
|
path={path}
|
||||||
/>
|
component={component}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
<Route path="*" component={NotFound} />
|
<Route path="*" component={NotFound} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
</KeyboardHotkeysProvider>
|
||||||
</DashboardProvider>
|
</DashboardProvider>
|
||||||
</QueryBuilderProvider>
|
</QueryBuilderProvider>
|
||||||
</ResourceProvider>
|
</ResourceProvider>
|
||||||
|
@ -15,9 +15,20 @@ export const ServiceMapPage = Loadable(
|
|||||||
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
|
() => import(/* webpackChunkName: "ServiceMapPage" */ 'modules/Servicemap'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const LogsSaveViews = Loadable(
|
||||||
|
() => import(/* webpackChunkName: "LogsSaveViews" */ 'pages/LogsModulePage'), // TODO: Add a wrapper so that the same component can be used in traces
|
||||||
|
);
|
||||||
|
|
||||||
export const TracesExplorer = Loadable(
|
export const TracesExplorer = Loadable(
|
||||||
() =>
|
() =>
|
||||||
import(/* webpackChunkName: "Traces Explorer Page" */ 'pages/TracesExplorer'),
|
import(
|
||||||
|
/* webpackChunkName: "Traces Explorer Page" */ 'pages/TracesModulePage'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TracesSaveViews = Loadable(
|
||||||
|
() =>
|
||||||
|
import(/* webpackChunkName: "Traces Save Views" */ 'pages/TracesModulePage'),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const TraceFilter = Loadable(
|
export const TraceFilter = Loadable(
|
||||||
@ -171,3 +182,7 @@ export const WorkspaceBlocked = Loadable(
|
|||||||
() =>
|
() =>
|
||||||
import(/* webpackChunkName: "WorkspaceLocked" */ 'pages/WorkspaceLocked'),
|
import(/* webpackChunkName: "WorkspaceLocked" */ 'pages/WorkspaceLocked'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const ShortcutsPage = Loadable(
|
||||||
|
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
|
||||||
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
|
import Shortcuts from 'pages/Shortcuts/Shortcuts';
|
||||||
import WorkspaceBlocked from 'pages/WorkspaceLocked';
|
import WorkspaceBlocked from 'pages/WorkspaceLocked';
|
||||||
import { RouteProps } from 'react-router-dom';
|
import { RouteProps } from 'react-router-dom';
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ import {
|
|||||||
Logs,
|
Logs,
|
||||||
LogsExplorer,
|
LogsExplorer,
|
||||||
LogsIndexToFields,
|
LogsIndexToFields,
|
||||||
|
LogsSaveViews,
|
||||||
MySettings,
|
MySettings,
|
||||||
NewDashboardPage,
|
NewDashboardPage,
|
||||||
OldLogsExplorer,
|
OldLogsExplorer,
|
||||||
@ -39,6 +41,7 @@ import {
|
|||||||
TraceDetail,
|
TraceDetail,
|
||||||
TraceFilter,
|
TraceFilter,
|
||||||
TracesExplorer,
|
TracesExplorer,
|
||||||
|
TracesSaveViews,
|
||||||
UnAuthorized,
|
UnAuthorized,
|
||||||
UsageExplorerPage,
|
UsageExplorerPage,
|
||||||
} from './pageComponents';
|
} from './pageComponents';
|
||||||
@ -86,6 +89,13 @@ const routes: AppRoutes[] = [
|
|||||||
exact: true,
|
exact: true,
|
||||||
key: 'SERVICE_MAP',
|
key: 'SERVICE_MAP',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.LOGS_SAVE_VIEWS,
|
||||||
|
component: LogsSaveViews,
|
||||||
|
isPrivate: true,
|
||||||
|
exact: true,
|
||||||
|
key: 'LOGS_SAVE_VIEWS',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.TRACE_DETAIL,
|
path: ROUTES.TRACE_DETAIL,
|
||||||
exact: true,
|
exact: true,
|
||||||
@ -163,6 +173,13 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'TRACES_EXPLORER',
|
key: 'TRACES_EXPLORER',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.TRACES_SAVE_VIEWS,
|
||||||
|
exact: true,
|
||||||
|
component: TracesSaveViews,
|
||||||
|
isPrivate: true,
|
||||||
|
key: 'TRACES_SAVE_VIEWS',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ROUTES.CHANNELS_NEW,
|
path: ROUTES.CHANNELS_NEW,
|
||||||
exact: true,
|
exact: true,
|
||||||
@ -303,6 +320,13 @@ const routes: AppRoutes[] = [
|
|||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
key: 'WORKSPACE_LOCKED',
|
key: 'WORKSPACE_LOCKED',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: ROUTES.SHORTCUTS,
|
||||||
|
exact: true,
|
||||||
|
component: Shortcuts,
|
||||||
|
isPrivate: true,
|
||||||
|
key: 'SHORTCUTS',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SUPPORT_ROUTE: AppRoutes = {
|
export const SUPPORT_ROUTE: AppRoutes = {
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
.timeSelection-input {
|
.timeSelection-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
height: 33px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
padding-left: 0px !important;
|
padding-left: 0px !important;
|
||||||
@ -59,6 +60,26 @@
|
|||||||
font-weight: 400 !important;
|
font-weight: 400 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4px;
|
||||||
|
cursor: default;
|
||||||
|
color: var(--bg-vanilla-400, #c0c1c3) !important;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: normal;
|
||||||
|
letter-spacing: 0.14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text:hover {
|
||||||
|
&.ant-btn-text {
|
||||||
|
background-color: unset !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.lightMode {
|
.lightMode {
|
||||||
.time-options-container {
|
.time-options-container {
|
||||||
.time-options-item {
|
.time-options-item {
|
||||||
@ -93,4 +114,8 @@
|
|||||||
color: rgba($color: #000000, $alpha: 0.4);
|
color: rgba($color: #000000, $alpha: 0.4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
color: var(--bg-slate-400) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,22 +4,44 @@ import './CustomTimePicker.styles.scss';
|
|||||||
|
|
||||||
import { Input, Popover, Tooltip, Typography } from 'antd';
|
import { Input, Popover, Tooltip, Typography } from 'antd';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||||
import { Options } from 'container/TopNav/DateTimeSelection/config';
|
import { Options } from 'container/TopNav/DateTimeSelection/config';
|
||||||
|
import {
|
||||||
|
FixedDurationSuggestionOptions,
|
||||||
|
RelativeDurationSuggestionOptions,
|
||||||
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import { defaultTo, noop } from 'lodash-es';
|
||||||
import debounce from 'lodash-es/debounce';
|
import debounce from 'lodash-es/debounce';
|
||||||
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
|
import { CheckCircle, ChevronDown, Clock } from 'lucide-react';
|
||||||
import { ChangeEvent, useEffect, useState } from 'react';
|
import {
|
||||||
|
ChangeEvent,
|
||||||
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
|
import CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
|
||||||
|
|
||||||
const maxAllowedMinTimeInMonths = 6;
|
const maxAllowedMinTimeInMonths = 6;
|
||||||
|
|
||||||
interface CustomTimePickerProps {
|
interface CustomTimePickerProps {
|
||||||
onSelect: (value: string) => void;
|
onSelect: (value: string) => void;
|
||||||
onError: (value: boolean) => void;
|
onError: (value: boolean) => void;
|
||||||
items: any[];
|
|
||||||
selectedValue: string;
|
selectedValue: string;
|
||||||
selectedTime: string;
|
selectedTime: string;
|
||||||
onValidCustomDateChange: ([t1, t2]: any[]) => void;
|
onValidCustomDateChange: ([t1, t2]: any[]) => void;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
items: any[];
|
||||||
|
newPopover?: boolean;
|
||||||
|
customDateTimeVisible?: boolean;
|
||||||
|
setCustomDTPickerVisible?: Dispatch<SetStateAction<boolean>>;
|
||||||
|
onCustomDateHandler?: (dateTimeRange: DateTimeRangeType) => void;
|
||||||
|
handleGoLive?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CustomTimePicker({
|
function CustomTimePicker({
|
||||||
@ -28,9 +50,15 @@ function CustomTimePicker({
|
|||||||
items,
|
items,
|
||||||
selectedValue,
|
selectedValue,
|
||||||
selectedTime,
|
selectedTime,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
onValidCustomDateChange,
|
onValidCustomDateChange,
|
||||||
|
newPopover,
|
||||||
|
customDateTimeVisible,
|
||||||
|
setCustomDTPickerVisible,
|
||||||
|
onCustomDateHandler,
|
||||||
|
handleGoLive,
|
||||||
}: CustomTimePickerProps): JSX.Element {
|
}: CustomTimePickerProps): JSX.Element {
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [
|
const [
|
||||||
selectedTimePlaceholderValue,
|
selectedTimePlaceholderValue,
|
||||||
setSelectedTimePlaceholderValue,
|
setSelectedTimePlaceholderValue,
|
||||||
@ -41,6 +69,7 @@ function CustomTimePicker({
|
|||||||
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
|
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const location = useLocation();
|
||||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||||
|
|
||||||
const getSelectedTimeRangeLabel = (
|
const getSelectedTimeRangeLabel = (
|
||||||
@ -56,6 +85,20 @@ function CustomTimePicker({
|
|||||||
return Options[index].label;
|
return Options[index].label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (
|
||||||
|
let index = 0;
|
||||||
|
index < RelativeDurationSuggestionOptions.length;
|
||||||
|
index++
|
||||||
|
) {
|
||||||
|
if (RelativeDurationSuggestionOptions[index].value === selectedTime) {
|
||||||
|
return RelativeDurationSuggestionOptions[index].label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let index = 0; index < FixedDurationSuggestionOptions.length; index++) {
|
||||||
|
if (FixedDurationSuggestionOptions[index].value === selectedTime) {
|
||||||
|
return FixedDurationSuggestionOptions[index].label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
@ -111,7 +154,7 @@ function CustomTimePicker({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minTime && minTime < maxAllowedMinTime) {
|
if (minTime && (!minTime.isValid() || minTime < maxAllowedMinTime)) {
|
||||||
setInputStatus('error');
|
setInputStatus('error');
|
||||||
onError(true);
|
onError(true);
|
||||||
setInputErrorMessage('Please enter time less than 6 months');
|
setInputErrorMessage('Please enter time less than 6 months');
|
||||||
@ -140,19 +183,25 @@ function CustomTimePicker({
|
|||||||
debouncedHandleInputChange(inputValue);
|
debouncedHandleInputChange(inputValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSelect = (label: string, value: string): void => {
|
||||||
|
onSelect(value);
|
||||||
|
setSelectedTimePlaceholderValue(label);
|
||||||
|
setInputStatus('');
|
||||||
|
onError(false);
|
||||||
|
setInputErrorMessage(null);
|
||||||
|
setInputValue('');
|
||||||
|
if (value !== 'custom') {
|
||||||
|
hide();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<div className="time-selection-dropdown-content">
|
<div className="time-selection-dropdown-content">
|
||||||
<div className="time-options-container">
|
<div className="time-options-container">
|
||||||
{items.map(({ value, label }) => (
|
{items?.map(({ value, label }) => (
|
||||||
<div
|
<div
|
||||||
onClick={(): void => {
|
onClick={(): void => {
|
||||||
onSelect(value);
|
handleSelect(label, value);
|
||||||
setSelectedTimePlaceholderValue(label);
|
|
||||||
setInputStatus('');
|
|
||||||
onError(false);
|
|
||||||
setInputErrorMessage(null);
|
|
||||||
setInputValue('');
|
|
||||||
hide();
|
|
||||||
}}
|
}}
|
||||||
key={value}
|
key={value}
|
||||||
className={cx(
|
className={cx(
|
||||||
@ -175,6 +224,15 @@ function CustomTimePicker({
|
|||||||
setIsInputFocused(false);
|
setIsInputFocused(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// this is required as TopNav component wraps the components and we need to clear the state on path change
|
||||||
|
useEffect(() => {
|
||||||
|
setInputStatus('');
|
||||||
|
onError(false);
|
||||||
|
setInputErrorMessage(null);
|
||||||
|
setInputValue('');
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="custom-time-picker">
|
<div className="custom-time-picker">
|
||||||
<Popover
|
<Popover
|
||||||
@ -184,11 +242,27 @@ function CustomTimePicker({
|
|||||||
)}
|
)}
|
||||||
placement="bottomRight"
|
placement="bottomRight"
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
content={content}
|
rootClassName="date-time-root"
|
||||||
|
content={
|
||||||
|
newPopover ? (
|
||||||
|
<CustomTimePickerPopoverContent
|
||||||
|
setIsOpen={setOpen}
|
||||||
|
customDateTimeVisible={defaultTo(customDateTimeVisible, false)}
|
||||||
|
setCustomDTPickerVisible={defaultTo(setCustomDTPickerVisible, noop)}
|
||||||
|
onCustomDateHandler={defaultTo(onCustomDateHandler, noop)}
|
||||||
|
onSelectHandler={handleSelect}
|
||||||
|
handleGoLive={defaultTo(handleGoLive, noop)}
|
||||||
|
options={items}
|
||||||
|
selectedTime={selectedTime}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)
|
||||||
|
}
|
||||||
arrow={false}
|
arrow={false}
|
||||||
|
trigger="hover"
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
trigger={['click']}
|
|
||||||
style={{
|
style={{
|
||||||
padding: 0,
|
padding: 0,
|
||||||
}}
|
}}
|
||||||
@ -236,3 +310,11 @@ function CustomTimePicker({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default CustomTimePicker;
|
export default CustomTimePicker;
|
||||||
|
|
||||||
|
CustomTimePicker.defaultProps = {
|
||||||
|
newPopover: false,
|
||||||
|
customDateTimeVisible: false,
|
||||||
|
setCustomDTPickerVisible: noop,
|
||||||
|
onCustomDateHandler: noop,
|
||||||
|
handleGoLive: noop,
|
||||||
|
};
|
||||||
|
@ -0,0 +1,133 @@
|
|||||||
|
import './CustomTimePicker.styles.scss';
|
||||||
|
|
||||||
|
import { Button, DatePicker } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||||
|
import {
|
||||||
|
Option,
|
||||||
|
RelativeDurationSuggestionOptions,
|
||||||
|
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
|
import dayjs, { Dayjs } from 'dayjs';
|
||||||
|
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { AppState } from 'store/reducers';
|
||||||
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||||
|
|
||||||
|
interface CustomTimePickerPopoverContentProps {
|
||||||
|
options: any[];
|
||||||
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
customDateTimeVisible: boolean;
|
||||||
|
setCustomDTPickerVisible: Dispatch<SetStateAction<boolean>>;
|
||||||
|
onCustomDateHandler: (dateTimeRange: DateTimeRangeType) => void;
|
||||||
|
onSelectHandler: (label: string, value: string) => void;
|
||||||
|
handleGoLive: () => void;
|
||||||
|
selectedTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CustomTimePickerPopoverContent({
|
||||||
|
options,
|
||||||
|
setIsOpen,
|
||||||
|
customDateTimeVisible,
|
||||||
|
setCustomDTPickerVisible,
|
||||||
|
onCustomDateHandler,
|
||||||
|
onSelectHandler,
|
||||||
|
handleGoLive,
|
||||||
|
selectedTime,
|
||||||
|
}: CustomTimePickerPopoverContentProps): JSX.Element {
|
||||||
|
const { RangePicker } = DatePicker;
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
|
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||||
|
(state) => state.globalTime,
|
||||||
|
);
|
||||||
|
|
||||||
|
const isLogsExplorerPage = useMemo(() => pathname === ROUTES.LOGS_EXPLORER, [
|
||||||
|
pathname,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const disabledDate = (current: Dayjs): boolean => {
|
||||||
|
const currentDay = dayjs(current);
|
||||||
|
return currentDay.isAfter(dayjs());
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPopoverClose = (visible: boolean): void => {
|
||||||
|
if (!visible) {
|
||||||
|
setCustomDTPickerVisible(false);
|
||||||
|
}
|
||||||
|
setIsOpen(visible);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onModalOkHandler = (date_time: any): void => {
|
||||||
|
if (date_time?.[1]) {
|
||||||
|
onPopoverClose(false);
|
||||||
|
}
|
||||||
|
onCustomDateHandler(date_time);
|
||||||
|
};
|
||||||
|
function getTimeChips(options: Option[]): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="relative-date-time-section">
|
||||||
|
{options.map((option) => (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
className="time-btns"
|
||||||
|
key={option.label + option.value}
|
||||||
|
onClick={(): void => {
|
||||||
|
onSelectHandler(option.label, option.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="date-time-popover">
|
||||||
|
<div className="date-time-options">
|
||||||
|
{isLogsExplorerPage && (
|
||||||
|
<Button className="data-time-live" type="text" onClick={handleGoLive}>
|
||||||
|
Live
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{options.map((option) => (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
key={option.label + option.value}
|
||||||
|
onClick={(): void => {
|
||||||
|
onSelectHandler(option.label, option.value);
|
||||||
|
}}
|
||||||
|
className={cx(
|
||||||
|
'date-time-options-btn',
|
||||||
|
selectedTime === option.value && 'active',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="relative-date-time">
|
||||||
|
{selectedTime === 'custom' || customDateTimeVisible ? (
|
||||||
|
<RangePicker
|
||||||
|
disabledDate={disabledDate}
|
||||||
|
allowClear
|
||||||
|
onCalendarChange={onModalOkHandler}
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...(selectedTime === 'custom' && {
|
||||||
|
defaultValue: [dayjs(minTime / 1000000), dayjs(maxTime / 1000000)],
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div className="time-heading">RELATIVE TIMES</div>
|
||||||
|
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomTimePickerPopoverContent;
|
@ -6,7 +6,6 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
Col,
|
Col,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
MenuProps,
|
MenuProps,
|
||||||
@ -152,95 +151,100 @@ function ExplorerCard({
|
|||||||
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
|
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
|
||||||
const saveButtonIcon = isQueryUpdated ? null : <SaveOutlined />;
|
const saveButtonIcon = isQueryUpdated ? null : <SaveOutlined />;
|
||||||
|
|
||||||
|
const showSaveView = false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ExplorerCardHeadContainer size="small">
|
{showSaveView && (
|
||||||
<Row align="middle">
|
<ExplorerCardHeadContainer size="small">
|
||||||
<Col span={6}>
|
<Row align="middle">
|
||||||
<Space>
|
<Col span={6}>
|
||||||
<Typography>Query Builder</Typography>
|
<Space>
|
||||||
<TextToolTip
|
<Typography>Query Builder</Typography>
|
||||||
url={ExploreHeaderToolTip.url}
|
<TextToolTip
|
||||||
text={ExploreHeaderToolTip.text}
|
url={ExploreHeaderToolTip.url}
|
||||||
useFilledIcon={false}
|
text={ExploreHeaderToolTip.text}
|
||||||
/>
|
useFilledIcon={false}
|
||||||
</Space>
|
/>
|
||||||
</Col>
|
</Space>
|
||||||
<OffSetCol span={18}>
|
</Col>
|
||||||
<Space size="large">
|
<OffSetCol span={18}>
|
||||||
{viewsData?.data.data && viewsData?.data.data.length && (
|
<Space size="large">
|
||||||
<Space>
|
{viewsData?.data.data && viewsData?.data.data.length && (
|
||||||
<Select
|
<Space>
|
||||||
getPopupContainer={popupContainer}
|
<Select
|
||||||
loading={isLoading || isRefetching}
|
getPopupContainer={popupContainer}
|
||||||
showSearch
|
loading={isLoading || isRefetching}
|
||||||
placeholder="Select a view"
|
showSearch
|
||||||
dropdownStyle={DropDownOverlay}
|
placeholder="Select a view"
|
||||||
dropdownMatchSelectWidth={false}
|
dropdownStyle={DropDownOverlay}
|
||||||
optionLabelProp="value"
|
dropdownMatchSelectWidth={false}
|
||||||
value={viewName || undefined}
|
optionLabelProp="value"
|
||||||
|
value={viewName || undefined}
|
||||||
|
>
|
||||||
|
{viewsData?.data.data.map((view) => (
|
||||||
|
<Select.Option key={view.uuid} value={view.name}>
|
||||||
|
<MenuItemGenerator
|
||||||
|
viewName={view.name}
|
||||||
|
viewKey={viewKey}
|
||||||
|
createdBy={view.createdBy}
|
||||||
|
uuid={view.uuid}
|
||||||
|
refetchAllView={refetchAllView}
|
||||||
|
viewData={viewsData.data.data}
|
||||||
|
sourcePage={sourcepage}
|
||||||
|
/>
|
||||||
|
</Select.Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
{isQueryUpdated && (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
onClick={onUpdateQueryHandler}
|
||||||
>
|
>
|
||||||
{viewsData?.data.data.map((view) => (
|
Save changes
|
||||||
<Select.Option key={view.uuid} value={view.name}>
|
</Button>
|
||||||
<MenuItemGenerator
|
)}
|
||||||
viewName={view.name}
|
<Popover
|
||||||
viewKey={viewKey}
|
getPopupContainer={popupContainer}
|
||||||
createdBy={view.createdBy}
|
placement="bottomLeft"
|
||||||
uuid={view.uuid}
|
trigger="click"
|
||||||
refetchAllView={refetchAllView}
|
content={
|
||||||
viewData={viewsData.data.data}
|
<SaveViewWithName
|
||||||
sourcePage={sourcepage}
|
sourcePage={sourcepage}
|
||||||
/>
|
handlePopOverClose={handleOpenChange}
|
||||||
</Select.Option>
|
refetchAllView={refetchAllView}
|
||||||
))}
|
/>
|
||||||
</Select>
|
}
|
||||||
</Space>
|
showArrow={false}
|
||||||
)}
|
open={isOpen}
|
||||||
{isQueryUpdated && (
|
onOpenChange={handleOpenChange}
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<SaveOutlined />}
|
|
||||||
onClick={onUpdateQueryHandler}
|
|
||||||
>
|
>
|
||||||
Save changes
|
<Button
|
||||||
</Button>
|
type={saveButtonType}
|
||||||
)}
|
icon={saveButtonIcon}
|
||||||
<Popover
|
data-testid="traces-save-view-action"
|
||||||
getPopupContainer={popupContainer}
|
>
|
||||||
placement="bottomLeft"
|
{isQueryUpdated
|
||||||
trigger="click"
|
? SaveButtonText.SAVE_AS_NEW_VIEW
|
||||||
content={
|
: SaveButtonText.SAVE_VIEW}
|
||||||
<SaveViewWithName
|
</Button>
|
||||||
sourcePage={sourcepage}
|
</Popover>
|
||||||
handlePopOverClose={handleOpenChange}
|
<ShareAltOutlined onClick={onCopyUrlHandler} />
|
||||||
refetchAllView={refetchAllView}
|
{viewKey && (
|
||||||
/>
|
<Dropdown trigger={['click']} menu={moreOptionMenu}>
|
||||||
}
|
<MoreOutlined />
|
||||||
showArrow={false}
|
</Dropdown>
|
||||||
open={isOpen}
|
)}
|
||||||
onOpenChange={handleOpenChange}
|
</Space>
|
||||||
>
|
</OffSetCol>
|
||||||
<Button
|
</Row>
|
||||||
type={saveButtonType}
|
</ExplorerCardHeadContainer>
|
||||||
icon={saveButtonIcon}
|
)}
|
||||||
data-testid="traces-save-view-action"
|
|
||||||
>
|
<div>{children}</div>
|
||||||
{isQueryUpdated
|
|
||||||
? SaveButtonText.SAVE_AS_NEW_VIEW
|
|
||||||
: SaveButtonText.SAVE_VIEW}
|
|
||||||
</Button>
|
|
||||||
</Popover>
|
|
||||||
<ShareAltOutlined onClick={onCopyUrlHandler} />
|
|
||||||
{viewKey && (
|
|
||||||
<Dropdown trigger={['click']} menu={moreOptionMenu}>
|
|
||||||
<MoreOutlined />
|
|
||||||
</Dropdown>
|
|
||||||
)}
|
|
||||||
</Space>
|
|
||||||
</OffSetCol>
|
|
||||||
</Row>
|
|
||||||
</ExplorerCardHeadContainer>
|
|
||||||
<Card>{children}</Card>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import styled, { CSSProperties } from 'styled-components';
|
|||||||
|
|
||||||
export const ExplorerCardHeadContainer = styled(Card)`
|
export const ExplorerCardHeadContainer = styled(Card)`
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
|
padding: 0;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const OffSetCol = styled(Col)`
|
export const OffSetCol = styled(Col)`
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
import MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||||
import { DataSource } from 'types/common/queryBuilder';
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
@ -46,7 +46,7 @@ describe('ExplorerCard', () => {
|
|||||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
expect(screen.getByText('Query Builder')).toBeInTheDocument();
|
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a save view button', () => {
|
it('renders a save view button', () => {
|
||||||
@ -55,19 +55,6 @@ describe('ExplorerCard', () => {
|
|||||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||||
</MockQueryClientProvider>,
|
</MockQueryClientProvider>,
|
||||||
);
|
);
|
||||||
expect(screen.getByText('Save view')).toBeInTheDocument();
|
expect(screen.queryByText('Save view')).not.toBeInTheDocument();
|
||||||
});
|
|
||||||
|
|
||||||
it('should see all the view listed in dropdown', async () => {
|
|
||||||
const screen = render(
|
|
||||||
<ExplorerCard sourcepage={DataSource.TRACES}>Mock Children</ExplorerCard>,
|
|
||||||
);
|
|
||||||
const selectPlaceholder = screen.getByText('Select a view');
|
|
||||||
|
|
||||||
fireEvent.mouseDown(selectPlaceholder);
|
|
||||||
const viewNameText = await screen.getAllByText('View 1');
|
|
||||||
viewNameText.forEach((element) => {
|
|
||||||
expect(element).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
SaveViewHandlerProps,
|
SaveViewHandlerProps,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const showErrorNotification = (
|
export const showErrorNotification = (
|
||||||
notifications: NotificationInstance,
|
notifications: NotificationInstance,
|
||||||
err: Error,
|
err: Error,
|
||||||
): void => {
|
): void => {
|
||||||
@ -90,6 +90,14 @@ export const isQueryUpdatedInView = ({
|
|||||||
// Omitting id from aggregateAttribute and groupBy
|
// Omitting id from aggregateAttribute and groupBy
|
||||||
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
|
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
|
||||||
|
|
||||||
|
if (
|
||||||
|
updatedCurrentQuery?.builder === undefined ||
|
||||||
|
updatedCurrentQuery.clickhouse_sql === undefined ||
|
||||||
|
updatedCurrentQuery.promql === undefined
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
panelType !== currentPanelType ||
|
panelType !== currentPanelType ||
|
||||||
!isEqual(query.builder, updatedCurrentQuery?.builder) ||
|
!isEqual(query.builder, updatedCurrentQuery?.builder) ||
|
||||||
|
@ -3,8 +3,11 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
|||||||
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
|
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
|
import { VIEWS } from './constants';
|
||||||
|
|
||||||
export type LogDetailProps = {
|
export type LogDetailProps = {
|
||||||
log: ILog | null;
|
log: ILog | null;
|
||||||
|
selectedTab: VIEWS;
|
||||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||||
Pick<ActionItemProps, 'onClickActionItem'> &
|
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
|
||||||
Pick<DrawerProps, 'onClose'>;
|
Pick<DrawerProps, 'onClose'>;
|
||||||
|
230
frontend/src/components/LogDetail/LogDetails.styles.scss
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
.log-detail-drawer {
|
||||||
|
border-left: 1px solid var(--bg-slate-500);
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
box-shadow: -4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
.ant-drawer-header {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
border-bottom: 1px solid var(--bg-slate-500);
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-close {
|
||||||
|
margin-inline-end: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-body {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--text-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: var(--padding-1);
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-detail-drawer__log {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.log-body {
|
||||||
|
font-family: 'SF Mono';
|
||||||
|
font-family: 'Space Mono', monospace;
|
||||||
|
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--text-vanilla-400);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-type-indicator {
|
||||||
|
height: 24px;
|
||||||
|
border: 2px solid var(--bg-slate-400);
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-left: 0;
|
||||||
|
|
||||||
|
&.INFO {
|
||||||
|
border-color: #1d212d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.WARNING {
|
||||||
|
border-color: #ffcd56;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ERROR {
|
||||||
|
border-color: #e5484d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-overflow-shadow {
|
||||||
|
background: linear-gradient(270deg, #121317 10.4%, rgba(18, 19, 23, 0) 100%);
|
||||||
|
|
||||||
|
width: 196px;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-and-search {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin: 16px 0;
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.json-action-btn {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-tabs {
|
||||||
|
color: var(--text-vanilla-400);
|
||||||
|
|
||||||
|
.view-title {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--margin-2);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
width: 114px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab::before {
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected_view {
|
||||||
|
background: var(--bg-slate-300);
|
||||||
|
color: var(--text-vanilla-100);
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected_view::before {
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
margin-top: var(--margin-2);
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
height: 46px;
|
||||||
|
padding: var(--padding-1) var(--padding-2);
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-close {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.ant-drawer-header {
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-400);
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-detail-drawer {
|
||||||
|
.title {
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-detail-drawer__log {
|
||||||
|
.log-overflow-shadow {
|
||||||
|
background: linear-gradient(
|
||||||
|
270deg,
|
||||||
|
var(--bg-vanilla-100) 10.4%,
|
||||||
|
rgba(255, 255, 255, 0) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-type-indicator {
|
||||||
|
border: 2px solid var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio-button {
|
||||||
|
border: 1px solid var(--bg-vanilla-400);
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.views-tabs {
|
||||||
|
.tab {
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected_view {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
border: 1px solid var(--bg-slate-300);
|
||||||
|
color: var(--text-ink-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected_view::before {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
border-left: 1px solid var(--bg-slate-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-and-search {
|
||||||
|
.action-btn {
|
||||||
|
border: 1px solid var(--bg-vanilla-400);
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
border: 1px solid var(--bg-vanilla-200);
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
color: var(--text-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
.query-builder-search-wrapper {
|
||||||
|
margin-top: 10px;
|
||||||
|
height: 46px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
.ant-select-selector {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
import './QueryBuilderSearchWrapper.styles.scss';
|
||||||
|
|
||||||
|
import useInitialQuery from 'container/LogsExplorerContext/useInitialQuery';
|
||||||
|
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||||
|
import { Dispatch, SetStateAction, useEffect } from 'react';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
function QueryBuilderSearchWrapper({
|
||||||
|
log,
|
||||||
|
filters,
|
||||||
|
contextQuery,
|
||||||
|
isEdit,
|
||||||
|
suffixIcon,
|
||||||
|
setFilters,
|
||||||
|
setContextQuery,
|
||||||
|
}: QueryBuilderSearchWraperProps): JSX.Element {
|
||||||
|
const initialContextQuery = useInitialQuery(log);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setContextQuery(initialContextQuery);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSearch = (tagFilters: TagFilter): void => {
|
||||||
|
const tagFiltersLength = tagFilters.items.length;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!tagFiltersLength && (!filters || !filters.items.length)) ||
|
||||||
|
tagFiltersLength === filters?.items.length ||
|
||||||
|
!contextQuery
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const nextQuery: Query = {
|
||||||
|
...contextQuery,
|
||||||
|
builder: {
|
||||||
|
...contextQuery.builder,
|
||||||
|
queryData: contextQuery.builder.queryData.map((item) => ({
|
||||||
|
...item,
|
||||||
|
filters: tagFilters,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
setFilters({ ...tagFilters });
|
||||||
|
setContextQuery({ ...nextQuery });
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
|
if (!contextQuery || !isEdit) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryBuilderSearch
|
||||||
|
query={contextQuery?.builder.queryData[0]}
|
||||||
|
onChange={handleSearch}
|
||||||
|
className="query-builder-search-wrapper"
|
||||||
|
suffixIcon={suffixIcon}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueryBuilderSearchWraperProps {
|
||||||
|
log: ILog;
|
||||||
|
isEdit: boolean;
|
||||||
|
contextQuery: Query | undefined;
|
||||||
|
setContextQuery: Dispatch<SetStateAction<Query | undefined>>;
|
||||||
|
filters: TagFilter | null;
|
||||||
|
setFilters: Dispatch<SetStateAction<TagFilter | null>>;
|
||||||
|
suffixIcon?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilderSearchWrapper.defaultProps = {
|
||||||
|
suffixIcon: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueryBuilderSearchWrapper;
|
7
frontend/src/components/LogDetail/constants.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export const VIEW_TYPES = {
|
||||||
|
OVERVIEW: 'OVERVIEW',
|
||||||
|
JSON: 'JSON',
|
||||||
|
CONTEXT: 'CONTEXT',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type VIEWS = typeof VIEW_TYPES[keyof typeof VIEW_TYPES];
|
@ -1,50 +1,207 @@
|
|||||||
import { Drawer, Tabs } from 'antd';
|
/* eslint-disable sonarjs/cognitive-complexity */
|
||||||
import JSONView from 'container/LogDetailedView/JsonView';
|
import './LogDetails.styles.scss';
|
||||||
import TableView from 'container/LogDetailedView/TableView';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
|
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||||
|
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||||
|
import { RadioChangeEvent } from 'antd/lib';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
|
||||||
|
import ContextView from 'container/LogDetailedView/ContextView/ContextView';
|
||||||
|
import JSONView from 'container/LogDetailedView/JsonView';
|
||||||
|
import Overview from 'container/LogDetailedView/Overview';
|
||||||
|
import { aggregateAttributesResourcesToString } from 'container/LogDetailedView/utils';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import {
|
||||||
|
Braces,
|
||||||
|
Copy,
|
||||||
|
Filter,
|
||||||
|
HardHat,
|
||||||
|
Table,
|
||||||
|
TextSelect,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useCopyToClipboard } from 'react-use';
|
||||||
|
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
import { VIEW_TYPES, VIEWS } from './constants';
|
||||||
import { LogDetailProps } from './LogDetail.interfaces';
|
import { LogDetailProps } from './LogDetail.interfaces';
|
||||||
|
import QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
|
||||||
|
|
||||||
function LogDetail({
|
function LogDetail({
|
||||||
log,
|
log,
|
||||||
onClose,
|
onClose,
|
||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
onClickActionItem,
|
onClickActionItem,
|
||||||
|
selectedTab,
|
||||||
}: LogDetailProps): JSX.Element {
|
}: LogDetailProps): JSX.Element {
|
||||||
const items = useMemo(
|
const [, copyToClipboard] = useCopyToClipboard();
|
||||||
() => [
|
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
|
||||||
{
|
|
||||||
label: 'Table',
|
const [isFilterVisibile, setIsFilterVisible] = useState<boolean>(false);
|
||||||
key: '1',
|
|
||||||
children: log && (
|
const [contextQuery, setContextQuery] = useState<Query | undefined>();
|
||||||
<TableView
|
const [filters, setFilters] = useState<TagFilter | null>(null);
|
||||||
logData={log}
|
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||||
onAddToQuery={onAddToQuery}
|
|
||||||
onClickActionItem={onClickActionItem}
|
const isDarkMode = useIsDarkMode();
|
||||||
/>
|
|
||||||
),
|
const { notifications } = useNotifications();
|
||||||
},
|
|
||||||
{
|
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
|
||||||
label: 'JSON',
|
|
||||||
key: '2',
|
const handleModeChange = (e: RadioChangeEvent): void => {
|
||||||
children: log && <JSONView logData={log} />,
|
setSelectedView(e.target.value);
|
||||||
},
|
setIsEdit(false);
|
||||||
],
|
setIsFilterVisible(false);
|
||||||
[log, onAddToQuery, onClickActionItem],
|
};
|
||||||
);
|
|
||||||
|
const handleFilterVisible = (): void => {
|
||||||
|
setIsFilterVisible(!isFilterVisibile);
|
||||||
|
setIsEdit(!isEdit);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawerCloseHandler = (
|
||||||
|
e: React.MouseEvent | React.KeyboardEvent,
|
||||||
|
): void => {
|
||||||
|
if (onClose) {
|
||||||
|
onClose(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleJSONCopy = (): void => {
|
||||||
|
copyToClipboard(LogJsonData);
|
||||||
|
notifications.success({
|
||||||
|
message: 'Copied to clipboard',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!log) {
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logType = log?.attributes_string?.log_level || LogType.INFO;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
width="60%"
|
width="60%"
|
||||||
title="Log Details"
|
title={
|
||||||
|
<>
|
||||||
|
<Divider type="vertical" className={cx('log-type-indicator', LogType)} />
|
||||||
|
<Typography.Text className="title">Log details</Typography.Text>
|
||||||
|
</>
|
||||||
|
}
|
||||||
placement="right"
|
placement="right"
|
||||||
closable
|
// closable
|
||||||
onClose={onClose}
|
onClose={drawerCloseHandler}
|
||||||
open={log !== null}
|
open={log !== null}
|
||||||
style={{ overscrollBehavior: 'contain' }}
|
style={{
|
||||||
|
overscrollBehavior: 'contain',
|
||||||
|
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||||
|
}}
|
||||||
|
className="log-detail-drawer"
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
|
closeIcon={<X size={16} style={{ marginTop: Spacing.MARGIN_1 }} />}
|
||||||
>
|
>
|
||||||
<Tabs defaultActiveKey="1" items={items} />
|
<div className="log-detail-drawer__log">
|
||||||
|
<Divider type="vertical" className={cx('log-type-indicator', logType)} />
|
||||||
|
<Tooltip title={log?.body} placement="left">
|
||||||
|
<Typography.Text className="log-body">{log?.body}</Typography.Text>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<div className="log-overflow-shadow"> </div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="tabs-and-search">
|
||||||
|
<Radio.Group
|
||||||
|
className="views-tabs"
|
||||||
|
onChange={handleModeChange}
|
||||||
|
value={selectedView}
|
||||||
|
>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||||
|
selectedView === VIEW_TYPES.OVERVIEW ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={VIEW_TYPES.OVERVIEW}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<Table size={14} />
|
||||||
|
Overview
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button
|
||||||
|
className={selectedView === VIEW_TYPES.JSON ? 'selected_view tab' : 'tab'}
|
||||||
|
value={VIEW_TYPES.JSON}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<Braces size={14} />
|
||||||
|
JSON
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
<Radio.Button
|
||||||
|
className={
|
||||||
|
selectedView === VIEW_TYPES.CONTEXT ? 'selected_view tab' : 'tab'
|
||||||
|
}
|
||||||
|
value={VIEW_TYPES.CONTEXT}
|
||||||
|
>
|
||||||
|
<div className="view-title">
|
||||||
|
<TextSelect size={14} />
|
||||||
|
Context
|
||||||
|
</div>
|
||||||
|
</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
|
||||||
|
{selectedView === VIEW_TYPES.JSON && (
|
||||||
|
<div className="json-action-btn">
|
||||||
|
<Button
|
||||||
|
className="action-btn"
|
||||||
|
icon={<Copy size={16} />}
|
||||||
|
onClick={handleJSONCopy}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedView === VIEW_TYPES.CONTEXT && (
|
||||||
|
<Button
|
||||||
|
className="action-btn"
|
||||||
|
icon={<Filter size={16} />}
|
||||||
|
onClick={handleFilterVisible}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<QueryBuilderSearchWrapper
|
||||||
|
isEdit={isEdit}
|
||||||
|
log={log}
|
||||||
|
filters={filters}
|
||||||
|
setContextQuery={setContextQuery}
|
||||||
|
setFilters={setFilters}
|
||||||
|
contextQuery={contextQuery}
|
||||||
|
suffixIcon={
|
||||||
|
<HardHat size={12} style={{ paddingRight: Spacing.PADDING_2 }} />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{selectedView === VIEW_TYPES.OVERVIEW && (
|
||||||
|
<Overview
|
||||||
|
logData={log}
|
||||||
|
onAddToQuery={onAddToQuery}
|
||||||
|
onClickActionItem={onClickActionItem}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedView === VIEW_TYPES.JSON && <JSONView logData={log} />}
|
||||||
|
|
||||||
|
{selectedView === VIEW_TYPES.CONTEXT && (
|
||||||
|
<ContextView
|
||||||
|
log={log}
|
||||||
|
filters={filters}
|
||||||
|
contextQuery={contextQuery}
|
||||||
|
isEdit={isEdit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
3
frontend/src/components/Logs/AddToQueryHOC.styles.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.addToQueryContainer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
@ -1,9 +1,9 @@
|
|||||||
|
import './AddToQueryHOC.styles.scss';
|
||||||
|
|
||||||
import { Popover } from 'antd';
|
import { Popover } from 'antd';
|
||||||
import { OPERATORS } from 'constants/queryBuilder';
|
import { OPERATORS } from 'constants/queryBuilder';
|
||||||
import { memo, ReactNode, useCallback, useMemo } from 'react';
|
import { memo, ReactNode, useCallback, useMemo } from 'react';
|
||||||
|
|
||||||
import { ButtonContainer } from './styles';
|
|
||||||
|
|
||||||
function AddToQueryHOC({
|
function AddToQueryHOC({
|
||||||
fieldKey,
|
fieldKey,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
@ -19,11 +19,12 @@ function AddToQueryHOC({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ButtonContainer size="small" type="text" onClick={handleQueryAdd}>
|
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
|
||||||
|
<div className="addToQueryContainer" onClick={handleQueryAdd}>
|
||||||
<Popover placement="top" content={popOverContent}>
|
<Popover placement="top" content={popOverContent}>
|
||||||
{children}
|
{children}
|
||||||
</Popover>
|
</Popover>
|
||||||
</ButtonContainer>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
103
frontend/src/components/Logs/ListLogView/ListLogView.styles.scss
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
.log-field-key {
|
||||||
|
padding-right: 5px;
|
||||||
|
color: var(--text-vanilla-400, #c0c1c3);
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
.log-value {
|
||||||
|
color: var(--text-vanilla-400, #c0c1c3);
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
.log-line {
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
.log-state-indicator {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
transition: background-color 0.2s ease-in;
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(171, 189, 255, 0.04) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-selected-fields {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.selected-log-field-key {
|
||||||
|
color: var(--bg-robin-400) !important;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-log-value {
|
||||||
|
color: var(--bg-sienna-500);
|
||||||
|
border-radius: 2px;
|
||||||
|
background: rgba(173, 127, 88, 0.08);
|
||||||
|
padding: 0px 2px;
|
||||||
|
margin-left: 7px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-log-kv {
|
||||||
|
min-height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-action-buttons {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 32px;
|
||||||
|
width: 68px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||||
|
background: var(--bg-ink-400, #121317);
|
||||||
|
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
.context-btn {
|
||||||
|
width: 50% !important;
|
||||||
|
}
|
||||||
|
.copy-link-btn {
|
||||||
|
width: 50% !important;
|
||||||
|
border-left: 1px solid var(--bg-slate-400, #1d212d) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-btn-default {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.log-field-key {
|
||||||
|
color: var(--text-slate-400);
|
||||||
|
}
|
||||||
|
.log-value {
|
||||||
|
color: var(--text-slate-400);
|
||||||
|
}
|
||||||
|
.log-line {
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--text-vanilla-200) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,35 +1,32 @@
|
|||||||
import { blue, grey, orange } from '@ant-design/colors';
|
import './ListLogView.styles.scss';
|
||||||
import {
|
|
||||||
CopyFilled,
|
import { blue } from '@ant-design/colors';
|
||||||
ExpandAltOutlined,
|
|
||||||
LinkOutlined,
|
|
||||||
MonitorOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import Convert from 'ansi-to-html';
|
import Convert from 'ansi-to-html';
|
||||||
import { Button, Divider, Row, Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
import LogDetail from 'components/LogDetail';
|
||||||
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
|
||||||
// utils
|
// utils
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
import { useCopyToClipboard } from 'react-use';
|
|
||||||
// interfaces
|
// interfaces
|
||||||
import { IField } from 'types/api/logs/fields';
|
import { IField } from 'types/api/logs/fields';
|
||||||
import { ILog } from 'types/api/logs/log';
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
|
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
|
||||||
import CopyClipboardHOC from '../CopyClipboardHOC';
|
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
||||||
|
import LogStateIndicator, {
|
||||||
|
LogType,
|
||||||
|
} from '../LogStateIndicator/LogStateIndicator';
|
||||||
// styles
|
// styles
|
||||||
import {
|
import {
|
||||||
Container,
|
Container,
|
||||||
LogContainer,
|
LogContainer,
|
||||||
LogText,
|
LogText,
|
||||||
SelectedLog,
|
|
||||||
Text,
|
Text,
|
||||||
TextContainer,
|
TextContainer,
|
||||||
} from './styles';
|
} from './styles';
|
||||||
@ -55,12 +52,10 @@ function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TextContainer>
|
<TextContainer>
|
||||||
<Text ellipsis type="secondary">
|
<Text ellipsis type="secondary" className="log-field-key">
|
||||||
{`${fieldKey}: `}
|
{`${fieldKey} : `}
|
||||||
</Text>
|
</Text>
|
||||||
<CopyClipboardHOC textToCopy={fieldValue}>
|
<LogText dangerouslySetInnerHTML={html} className="log-value" />
|
||||||
<LogText dangerouslySetInnerHTML={html} />
|
|
||||||
</CopyClipboardHOC>
|
|
||||||
</TextContainer>
|
</TextContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -71,23 +66,23 @@ function LogSelectedField({
|
|||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
}: LogSelectedFieldProps): JSX.Element {
|
}: LogSelectedFieldProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<SelectedLog>
|
<div className="log-selected-fields">
|
||||||
<AddToQueryHOC
|
<AddToQueryHOC
|
||||||
fieldKey={fieldKey}
|
fieldKey={fieldKey}
|
||||||
fieldValue={fieldValue}
|
fieldValue={fieldValue}
|
||||||
onAddToQuery={onAddToQuery}
|
onAddToQuery={onAddToQuery}
|
||||||
>
|
>
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
<span style={{ color: blue[4] }}>{fieldKey}</span>
|
<span style={{ color: blue[4] }} className="selected-log-field-key">
|
||||||
|
{fieldKey}
|
||||||
|
</span>
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</AddToQueryHOC>
|
</AddToQueryHOC>
|
||||||
<CopyClipboardHOC textToCopy={fieldValue}>
|
<Typography.Text ellipsis className="selected-log-kv">
|
||||||
<Typography.Text ellipsis>
|
<span className="selected-log-field-key">{': '}</span>
|
||||||
<span>{': '}</span>
|
<span className="selected-log-value">{fieldValue || "''"}</span>
|
||||||
<span style={{ color: orange[6] }}>{fieldValue || "''"}</span>
|
</Typography.Text>
|
||||||
</Typography.Text>
|
</div>
|
||||||
</CopyClipboardHOC>
|
|
||||||
</SelectedLog>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,6 +91,7 @@ type ListLogViewProps = {
|
|||||||
selectedFields: IField[];
|
selectedFields: IField[];
|
||||||
onSetActiveLog: (log: ILog) => void;
|
onSetActiveLog: (log: ILog) => void;
|
||||||
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
|
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
|
||||||
|
activeLog?: ILog | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function ListLogView({
|
function ListLogView({
|
||||||
@ -103,34 +99,42 @@ function ListLogView({
|
|||||||
selectedFields,
|
selectedFields,
|
||||||
onSetActiveLog,
|
onSetActiveLog,
|
||||||
onAddToQuery,
|
onAddToQuery,
|
||||||
|
activeLog,
|
||||||
}: ListLogViewProps): JSX.Element {
|
}: ListLogViewProps): JSX.Element {
|
||||||
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
||||||
|
|
||||||
const [, setCopy] = useCopyToClipboard();
|
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
|
||||||
const { notifications } = useNotifications();
|
|
||||||
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
|
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
|
||||||
logData.id,
|
logData.id,
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
activeLog: activeContextLog,
|
activeLog: activeContextLog,
|
||||||
|
onAddToQuery: handleAddToQuery,
|
||||||
onSetActiveLog: handleSetActiveContextLog,
|
onSetActiveLog: handleSetActiveContextLog,
|
||||||
onClearActiveLog: handleClearActiveContextLog,
|
onClearActiveLog: handleClearActiveContextLog,
|
||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
|
|
||||||
|
const handlerClearActiveContextLog = useCallback(
|
||||||
|
(event: React.MouseEvent | React.KeyboardEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
handleClearActiveContextLog();
|
||||||
|
},
|
||||||
|
[handleClearActiveContextLog],
|
||||||
|
);
|
||||||
|
|
||||||
const handleDetailedView = useCallback(() => {
|
const handleDetailedView = useCallback(() => {
|
||||||
onSetActiveLog(logData);
|
onSetActiveLog(logData);
|
||||||
}, [logData, onSetActiveLog]);
|
}, [logData, onSetActiveLog]);
|
||||||
|
|
||||||
const handleShowContext = useCallback(() => {
|
const handleShowContext = useCallback(
|
||||||
handleSetActiveContextLog(logData);
|
(event: React.MouseEvent) => {
|
||||||
}, [logData, handleSetActiveContextLog]);
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
const handleCopyJSON = (): void => {
|
handleSetActiveContextLog(logData);
|
||||||
setCopy(JSON.stringify(logData, null, 2));
|
},
|
||||||
notifications.success({
|
[logData, handleSetActiveContextLog],
|
||||||
message: 'Copied to clipboard',
|
);
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatedSelecedFields = useMemo(
|
const updatedSelecedFields = useMemo(
|
||||||
() => selectedFields.filter((e) => e.name !== 'id'),
|
() => selectedFields.filter((e) => e.name !== 'id'),
|
||||||
@ -145,84 +149,74 @@ function ListLogView({
|
|||||||
[flattenLogData.timestamp],
|
[flattenLogData.timestamp],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const logType = logData?.attributes_string?.log_level || LogType.INFO;
|
||||||
|
|
||||||
|
const handleMouseEnter = (): void => {
|
||||||
|
setHasActionButtons(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (): void => {
|
||||||
|
setHasActionButtons(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container $isActiveLog={isHighlighted}>
|
<>
|
||||||
<div>
|
<Container
|
||||||
<LogContainer>
|
$isActiveLog={isHighlighted}
|
||||||
<>
|
onMouseEnter={handleMouseEnter}
|
||||||
<LogGeneralField fieldKey="log" fieldValue={flattenLogData.body} />
|
onMouseLeave={handleMouseLeave}
|
||||||
{flattenLogData.stream && (
|
onClick={handleDetailedView}
|
||||||
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
|
>
|
||||||
)}
|
<div className="log-line">
|
||||||
<LogGeneralField fieldKey="timestamp" fieldValue={timestampValue} />
|
<LogStateIndicator
|
||||||
</>
|
type={logType}
|
||||||
</LogContainer>
|
isActive={
|
||||||
<div>
|
activeLog?.id === logData.id || activeContextLog?.id === logData.id
|
||||||
{updatedSelecedFields.map((field) =>
|
}
|
||||||
isValidLogField(flattenLogData[field.name] as never) ? (
|
/>
|
||||||
<LogSelectedField
|
<div>
|
||||||
key={field.name}
|
<LogContainer>
|
||||||
fieldKey={field.name}
|
<LogGeneralField fieldKey="Log" fieldValue={flattenLogData.body} />
|
||||||
fieldValue={flattenLogData[field.name] as never}
|
{flattenLogData.stream && (
|
||||||
onAddToQuery={onAddToQuery}
|
<LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} />
|
||||||
/>
|
)}
|
||||||
) : null,
|
<LogGeneralField fieldKey="Timestamp" fieldValue={timestampValue} />
|
||||||
)}
|
|
||||||
|
{updatedSelecedFields.map((field) =>
|
||||||
|
isValidLogField(flattenLogData[field.name] as never) ? (
|
||||||
|
<LogSelectedField
|
||||||
|
key={field.name}
|
||||||
|
fieldKey={field.name}
|
||||||
|
fieldValue={flattenLogData[field.name] as never}
|
||||||
|
onAddToQuery={onAddToQuery}
|
||||||
|
/>
|
||||||
|
) : null,
|
||||||
|
)}
|
||||||
|
</LogContainer>
|
||||||
|
</div>
|
||||||
</div>
|
</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] }}
|
|
||||||
icon={<ExpandAltOutlined />}
|
|
||||||
>
|
|
||||||
View Details
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="text"
|
|
||||||
onClick={handleCopyJSON}
|
|
||||||
style={{ color: grey[1] }}
|
|
||||||
icon={<CopyFilled />}
|
|
||||||
>
|
|
||||||
Copy JSON
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{isLogsExplorerPage && (
|
{hasActionButtons && isLogsExplorerPage && (
|
||||||
<>
|
<LogLinesActionButtons
|
||||||
<Button
|
handleShowContext={handleShowContext}
|
||||||
size="small"
|
onLogCopy={onLogCopy}
|
||||||
type="text"
|
|
||||||
onClick={handleShowContext}
|
|
||||||
style={{ color: grey[1] }}
|
|
||||||
icon={<MonitorOutlined />}
|
|
||||||
>
|
|
||||||
Show in Context
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="text"
|
|
||||||
onClick={onLogCopy}
|
|
||||||
style={{ color: grey[1] }}
|
|
||||||
icon={<LinkOutlined />}
|
|
||||||
>
|
|
||||||
Copy Link
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeContextLog && (
|
|
||||||
<LogsExplorerContext
|
|
||||||
log={activeContextLog}
|
|
||||||
onClose={handleClearActiveContextLog}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Container>
|
||||||
</Container>
|
{activeContextLog && (
|
||||||
|
<LogDetail
|
||||||
|
log={activeContextLog}
|
||||||
|
onAddToQuery={handleAddToQuery}
|
||||||
|
selectedTab={VIEW_TYPES.CONTEXT}
|
||||||
|
onClose={handlerClearActiveContextLog}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ListLogView.defaultProps = {
|
||||||
|
activeLog: null,
|
||||||
|
};
|
||||||
|
|
||||||
export default ListLogView;
|
export default ListLogView;
|
||||||
|
@ -7,6 +7,7 @@ export const Container = styled(Card)<{
|
|||||||
}>`
|
}>`
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
|
cursor: pointer;
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
padding: 0.3rem 0.6rem;
|
padding: 0.3rem 0.6rem;
|
||||||
}
|
}
|
||||||
@ -29,11 +30,13 @@ export const TextContainer = styled.div`
|
|||||||
|
|
||||||
export const LogContainer = styled.div`
|
export const LogContainer = styled.div`
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LogText = styled.div`
|
export const LogText = styled.div`
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
.log-line-action-buttons {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
top: 50%;
|
||||||
|
right: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
|
||||||
|
.ant-btn-default {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 9px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&.active-tab {
|
||||||
|
background-color: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-log-btn {
|
||||||
|
border-left: 1px solid var(--bg-slate-400);
|
||||||
|
border-color: var(--bg-slate-400) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.log-line-action-buttons {
|
||||||
|
border: 1px solid var(--bg-vanilla-400);
|
||||||
|
background: var(--bg-vanilla-400);
|
||||||
|
|
||||||
|
.ant-btn-default {
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-log-btn {
|
||||||
|
border-left: 1px solid var(--bg-vanilla-400);
|
||||||
|
border-color: var(--bg-vanilla-400) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
import './LogLinesActionButtons.styles.scss';
|
||||||
|
|
||||||
|
import { LinkOutlined } from '@ant-design/icons';
|
||||||
|
import { Button, Tooltip } from 'antd';
|
||||||
|
import { TextSelect } from 'lucide-react';
|
||||||
|
import { MouseEventHandler } from 'react';
|
||||||
|
|
||||||
|
export interface LogLinesActionButtonsProps {
|
||||||
|
handleShowContext: MouseEventHandler<HTMLElement>;
|
||||||
|
onLogCopy: MouseEventHandler<HTMLElement>;
|
||||||
|
customClassName?: string;
|
||||||
|
}
|
||||||
|
export default function LogLinesActionButtons({
|
||||||
|
handleShowContext,
|
||||||
|
onLogCopy,
|
||||||
|
customClassName = '',
|
||||||
|
}: LogLinesActionButtonsProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className={`log-line-action-buttons ${customClassName}`}>
|
||||||
|
<Tooltip title="Show in Context">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<TextSelect size={14} />}
|
||||||
|
className="show-context-btn"
|
||||||
|
onClick={handleShowContext}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Copy Link">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<LinkOutlined size={14} />}
|
||||||
|
onClick={onLogCopy}
|
||||||
|
className="copy-log-btn"
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogLinesActionButtons.defaultProps = {
|
||||||
|
customClassName: '',
|
||||||
|
};
|
@ -0,0 +1,30 @@
|
|||||||
|
.log-state-indicator {
|
||||||
|
padding-left: 8px;
|
||||||
|
|
||||||
|
.line {
|
||||||
|
margin: 0 8px;
|
||||||
|
min-height: 24px;
|
||||||
|
height: 100%;
|
||||||
|
width: 3px;
|
||||||
|
border-radius: 50px;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
|
&.INFO {
|
||||||
|
background-color: #1d212d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.WARNING {
|
||||||
|
background-color: #ffcd56;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ERROR {
|
||||||
|
background-color: #e5484d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.isActive {
|
||||||
|
.line {
|
||||||
|
background-color: var(--bg-robin-400, #7190f9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
import './LogStateIndicator.styles.scss';
|
||||||
|
|
||||||
|
import cx from 'classnames';
|
||||||
|
|
||||||
|
export const LogType = {
|
||||||
|
INFO: 'INFO',
|
||||||
|
WARNING: 'WARNING',
|
||||||
|
ERROR: 'ERROR',
|
||||||
|
};
|
||||||
|
function LogStateIndicator({
|
||||||
|
type,
|
||||||
|
isActive,
|
||||||
|
}: {
|
||||||
|
type: string;
|
||||||
|
isActive?: boolean;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className={cx('log-state-indicator', isActive ? 'isActive' : '')}>
|
||||||
|
<div className={cx('line', type)}> </div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
LogStateIndicator.defaultProps = {
|
||||||
|
isActive: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogStateIndicator;
|
@ -1,11 +1,9 @@
|
|||||||
import {
|
import './RawLogView.styles.scss';
|
||||||
ExpandAltOutlined,
|
|
||||||
LinkOutlined,
|
|
||||||
MonitorOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import Convert from 'ansi-to-html';
|
import Convert from 'ansi-to-html';
|
||||||
import { Button, DrawerProps, Tooltip } from 'antd';
|
import { DrawerProps } from 'antd';
|
||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
|
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
||||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
@ -14,7 +12,7 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
|||||||
// hooks
|
// hooks
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { isEmpty, isUndefined } from 'lodash-es';
|
import { isEmpty, isNumber, isUndefined } from 'lodash-es';
|
||||||
import {
|
import {
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
MouseEvent,
|
MouseEvent,
|
||||||
@ -24,13 +22,12 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
||||||
|
import LogStateIndicator, {
|
||||||
|
LogType,
|
||||||
|
} from '../LogStateIndicator/LogStateIndicator';
|
||||||
// styles
|
// styles
|
||||||
import {
|
import { RawLogContent, RawLogViewContainer } from './styles';
|
||||||
ActionButtonsWrapper,
|
|
||||||
ExpandIconWrapper,
|
|
||||||
RawLogContent,
|
|
||||||
RawLogViewContainer,
|
|
||||||
} from './styles';
|
|
||||||
import { RawLogViewProps } from './types';
|
import { RawLogViewProps } from './types';
|
||||||
|
|
||||||
const convert = new Convert();
|
const convert = new Convert();
|
||||||
@ -50,7 +47,6 @@ function RawLogView({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
activeLog: activeContextLog,
|
activeLog: activeContextLog,
|
||||||
onSetActiveLog: handleSetActiveContextLog,
|
|
||||||
onClearActiveLog: handleClearActiveContextLog,
|
onClearActiveLog: handleClearActiveContextLog,
|
||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
const {
|
const {
|
||||||
@ -61,12 +57,15 @@ function RawLogView({
|
|||||||
} = useActiveLog();
|
} = useActiveLog();
|
||||||
|
|
||||||
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
|
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
|
||||||
|
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
|
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
|
||||||
|
|
||||||
const severityText = data.severity_text ? `${data.severity_text} |` : '';
|
const severityText = data.severity_text ? `${data.severity_text} |` : '';
|
||||||
|
|
||||||
|
const logType = data?.attributes_string?.log_level || LogType.INFO;
|
||||||
|
|
||||||
const updatedSelecedFields = useMemo(
|
const updatedSelecedFields = useMemo(
|
||||||
() => selectedFields.filter((e) => e.name !== 'id'),
|
() => selectedFields.filter((e) => e.name !== 'id'),
|
||||||
[selectedFields],
|
[selectedFields],
|
||||||
@ -74,7 +73,14 @@ function RawLogView({
|
|||||||
|
|
||||||
const attributesValues = updatedSelecedFields
|
const attributesValues = updatedSelecedFields
|
||||||
.map((field) => flattenLogData[field.name])
|
.map((field) => flattenLogData[field.name])
|
||||||
.filter((attribute) => !isUndefined(attribute) && !isEmpty(attribute));
|
.filter((attribute) => {
|
||||||
|
// loadash isEmpty doesnot work with numbers
|
||||||
|
if (isNumber(attribute)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !isUndefined(attribute) && !isEmpty(attribute);
|
||||||
|
});
|
||||||
|
|
||||||
let attributesText = attributesValues.join(' | ');
|
let attributesText = attributesValues.join(' | ');
|
||||||
|
|
||||||
@ -98,6 +104,7 @@ function RawLogView({
|
|||||||
if (activeContextLog || isReadOnly) return;
|
if (activeContextLog || isReadOnly) return;
|
||||||
|
|
||||||
onSetActiveLog(data);
|
onSetActiveLog(data);
|
||||||
|
setSelectedTab(VIEW_TYPES.OVERVIEW);
|
||||||
}, [activeContextLog, isReadOnly, data, onSetActiveLog]);
|
}, [activeContextLog, isReadOnly, data, onSetActiveLog]);
|
||||||
|
|
||||||
const handleCloseLogDetail: DrawerProps['onClose'] = useCallback(
|
const handleCloseLogDetail: DrawerProps['onClose'] = useCallback(
|
||||||
@ -108,6 +115,7 @@ function RawLogView({
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
|
||||||
onClearActiveLog();
|
onClearActiveLog();
|
||||||
|
setSelectedTab(undefined);
|
||||||
},
|
},
|
||||||
[onClearActiveLog],
|
[onClearActiveLog],
|
||||||
);
|
);
|
||||||
@ -128,9 +136,11 @@ function RawLogView({
|
|||||||
(event) => {
|
(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
handleSetActiveContextLog(data);
|
// handleSetActiveContextLog(data);
|
||||||
|
setSelectedTab(VIEW_TYPES.CONTEXT);
|
||||||
|
onSetActiveLog(data);
|
||||||
},
|
},
|
||||||
[data, handleSetActiveContextLog],
|
[data, onSetActiveLog],
|
||||||
);
|
);
|
||||||
|
|
||||||
const html = useMemo(
|
const html = useMemo(
|
||||||
@ -147,37 +157,30 @@ function RawLogView({
|
|||||||
align="middle"
|
align="middle"
|
||||||
$isDarkMode={isDarkMode}
|
$isDarkMode={isDarkMode}
|
||||||
$isReadOnly={isReadOnly}
|
$isReadOnly={isReadOnly}
|
||||||
$isActiveLog={isHighlighted}
|
$isHightlightedLog={isHighlighted}
|
||||||
|
$isActiveLog={isActiveLog}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onMouseLeave={handleMouseLeave}
|
onMouseLeave={handleMouseLeave}
|
||||||
>
|
>
|
||||||
{!isReadOnly && (
|
<LogStateIndicator
|
||||||
<ExpandIconWrapper flex="30px">
|
type={logType}
|
||||||
<ExpandAltOutlined />
|
isActive={activeLog?.id === data.id || activeContextLog?.id === data.id}
|
||||||
</ExpandIconWrapper>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<RawLogContent
|
<RawLogContent
|
||||||
$isReadOnly={isReadOnly}
|
$isReadOnly={isReadOnly}
|
||||||
$isActiveLog={isActiveLog}
|
$isActiveLog={isActiveLog}
|
||||||
|
$isDarkMode={isDarkMode}
|
||||||
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
|
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
|
||||||
linesPerRow={linesPerRow}
|
linesPerRow={linesPerRow}
|
||||||
dangerouslySetInnerHTML={html}
|
dangerouslySetInnerHTML={html}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasActionButtons && (
|
{hasActionButtons && (
|
||||||
<ActionButtonsWrapper>
|
<LogLinesActionButtons
|
||||||
<Tooltip title="Show Context">
|
handleShowContext={handleShowContext}
|
||||||
<Button
|
onLogCopy={onLogCopy}
|
||||||
size="small"
|
/>
|
||||||
icon={<MonitorOutlined />}
|
|
||||||
onClick={handleShowContext}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Copy Link">
|
|
||||||
<Button size="small" icon={<LinkOutlined />} onClick={onLogCopy} />
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButtonsWrapper>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeContextLog && (
|
{activeContextLog && (
|
||||||
@ -186,12 +189,15 @@ function RawLogView({
|
|||||||
onClose={handleClearActiveContextLog}
|
onClose={handleClearActiveContextLog}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<LogDetail
|
{selectedTab && (
|
||||||
log={activeLog}
|
<LogDetail
|
||||||
onClose={handleCloseLogDetail}
|
selectedTab={selectedTab}
|
||||||
onAddToQuery={onAddToQuery}
|
log={activeLog}
|
||||||
onClickActionItem={onAddToQuery}
|
onClose={handleCloseLogDetail}
|
||||||
/>
|
onAddToQuery={onAddToQuery}
|
||||||
|
onClickActionItem={onAddToQuery}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</RawLogViewContainer>
|
</RawLogViewContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { blue } from '@ant-design/colors';
|
import { blue } from '@ant-design/colors';
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
import { Col, Row, Space } from 'antd';
|
import { Col, Row, Space } from 'antd';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
|
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
|
||||||
@ -9,20 +10,25 @@ export const RawLogViewContainer = styled(Row)<{
|
|||||||
$isDarkMode: boolean;
|
$isDarkMode: boolean;
|
||||||
$isReadOnly?: boolean;
|
$isReadOnly?: boolean;
|
||||||
$isActiveLog?: boolean;
|
$isActiveLog?: boolean;
|
||||||
|
$isHightlightedLog: boolean;
|
||||||
}>`
|
}>`
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-weight: 700;
|
|
||||||
font-size: 0.625rem;
|
display: flex;
|
||||||
line-height: 1.25rem;
|
align-items: stretch;
|
||||||
|
|
||||||
transition: background-color 0.2s ease-in;
|
transition: background-color 0.2s ease-in;
|
||||||
|
|
||||||
|
.log-state-indicator {
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
||||||
|
|
||||||
${({ $isReadOnly, $isDarkMode, $isActiveLog }): string =>
|
${({ $isReadOnly, $isActiveLog, $isDarkMode }): string =>
|
||||||
$isActiveLog
|
$isActiveLog
|
||||||
? getActiveLogBackground()
|
? getActiveLogBackground($isActiveLog, $isDarkMode)
|
||||||
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
|
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -30,13 +36,17 @@ export const ExpandIconWrapper = styled(Col)`
|
|||||||
color: ${blue[6]};
|
color: ${blue[6]};
|
||||||
padding: 0.25rem 0.375rem;
|
padding: 0.25rem 0.375rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 12px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const RawLogContent = styled.div<RawLogContentProps>`
|
export const RawLogContent = styled.div<RawLogContentProps>`
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-family: Fira Code, monospace;
|
font-family: 'SF Mono', monospace;
|
||||||
font-weight: 300;
|
font-family: 'Space Mono', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
text-align: left;
|
||||||
|
color: ${({ $isDarkMode }): string =>
|
||||||
|
$isDarkMode ? Color.BG_VANILLA_400 : Color.BG_INK_400};
|
||||||
|
|
||||||
${({ $isTextOverflowEllipsisDisabled, linesPerRow }): string =>
|
${({ $isTextOverflowEllipsisDisabled, linesPerRow }): string =>
|
||||||
$isTextOverflowEllipsisDisabled
|
$isTextOverflowEllipsisDisabled
|
||||||
@ -48,15 +58,12 @@ export const RawLogContent = styled.div<RawLogContentProps>`
|
|||||||
line-clamp: ${linesPerRow};
|
line-clamp: ${linesPerRow};
|
||||||
-webkit-box-orient: vertical;`};
|
-webkit-box-orient: vertical;`};
|
||||||
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
letter-spacing: -0.07px;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
|
|
||||||
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
|
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
|
||||||
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};
|
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};
|
||||||
|
|
||||||
${({ $isActiveLog, $isReadOnly }): string =>
|
|
||||||
$isReadOnly && $isActiveLog ? 'padding: 0 1.5rem;' : ''}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ActionButtonsWrapper = styled(Space)`
|
export const ActionButtonsWrapper = styled(Space)`
|
||||||
|
@ -14,5 +14,6 @@ export interface RawLogContentProps {
|
|||||||
linesPerRow: number;
|
linesPerRow: number;
|
||||||
$isReadOnly?: boolean;
|
$isReadOnly?: boolean;
|
||||||
$isActiveLog?: boolean;
|
$isActiveLog?: boolean;
|
||||||
|
$isDarkMode?: boolean;
|
||||||
$isTextOverflowEllipsisDisabled?: boolean;
|
$isTextOverflowEllipsisDisabled?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,21 @@
|
|||||||
import { TableProps } from 'antd';
|
import { TableProps } from 'antd';
|
||||||
import { CSSProperties } from 'react';
|
import { CSSProperties } from 'react';
|
||||||
|
|
||||||
export const defaultCellStyle: CSSProperties = {
|
export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
|
||||||
paddingTop: 4,
|
return {
|
||||||
paddingBottom: 6,
|
paddingTop: 4,
|
||||||
paddingRight: 8,
|
paddingBottom: 6,
|
||||||
paddingLeft: 8,
|
paddingRight: 8,
|
||||||
};
|
paddingLeft: 8,
|
||||||
|
color: isDarkMode ? 'var(--bg-vanilla-400)' : 'var(--bg-slate-400)',
|
||||||
|
fontSize: '14px',
|
||||||
|
fontStyle: 'normal',
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: '18px',
|
||||||
|
letterSpacing: '-0.07px',
|
||||||
|
marginBottom: '0px',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const defaultTableStyle: CSSProperties = {
|
export const defaultTableStyle: CSSProperties = {
|
||||||
minWidth: '40rem',
|
minWidth: '40rem',
|
||||||
|
@ -2,18 +2,22 @@ import styled from 'styled-components';
|
|||||||
|
|
||||||
interface TableBodyContentProps {
|
interface TableBodyContentProps {
|
||||||
linesPerRow: number;
|
linesPerRow: number;
|
||||||
|
isDarkMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableBodyContent = styled.div<TableBodyContentProps>`
|
export const TableBodyContent = styled.div<TableBodyContentProps>`
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
color: ${(props): string =>
|
||||||
|
props.isDarkMode ? 'var(--bg-vanilla-400, #c0c1c3)' : 'var(--bg-slate-400)'};
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: ${(props): number => props.linesPerRow};
|
-webkit-line-clamp: ${(props): number => props.linesPerRow};
|
||||||
line-clamp: ${(props): number => props.linesPerRow};
|
line-clamp: ${(props): number => props.linesPerRow};
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
|
|
||||||
font-size: 0.875rem;
|
|
||||||
|
|
||||||
line-height: 2rem;
|
|
||||||
`;
|
`;
|
||||||
|
@ -22,6 +22,8 @@ export type UseTableViewProps = {
|
|||||||
appendTo?: 'center' | 'end';
|
appendTo?: 'center' | 'end';
|
||||||
onOpenLogsContext?: (log: ILog) => void;
|
onOpenLogsContext?: (log: ILog) => void;
|
||||||
onClickExpand?: (log: ILog) => void;
|
onClickExpand?: (log: ILog) => void;
|
||||||
|
activeLog?: ILog | null;
|
||||||
|
activeContextLog?: ILog | null;
|
||||||
} & LogsTableViewProps;
|
} & LogsTableViewProps;
|
||||||
|
|
||||||
export type ActionsColumnProps = {
|
export type ActionsColumnProps = {
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
.text {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-timestamp {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-state-indicator {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.text {
|
||||||
|
color: var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +1,21 @@
|
|||||||
import {
|
import './useTableView.styles.scss';
|
||||||
ExpandAltOutlined,
|
|
||||||
LinkOutlined,
|
|
||||||
MonitorOutlined,
|
|
||||||
} from '@ant-design/icons';
|
|
||||||
import Convert from 'ansi-to-html';
|
import Convert from 'ansi-to-html';
|
||||||
import { Button, Space, Typography } from 'antd';
|
import { Typography } from 'antd';
|
||||||
import { ColumnsType } from 'antd/es/table';
|
import { ColumnsType } from 'antd/es/table';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import dompurify from 'dompurify';
|
import dompurify from 'dompurify';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { defaultTo } from 'lodash-es';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
import { ExpandIconWrapper } from '../RawLogView/styles';
|
import LogStateIndicator, {
|
||||||
import { defaultCellStyle, defaultTableStyle } from './config';
|
LogType,
|
||||||
|
} from '../LogStateIndicator/LogStateIndicator';
|
||||||
|
import { defaultTableStyle, getDefaultCellStyle } from './config';
|
||||||
import { TableBodyContent } from './styles';
|
import { TableBodyContent } from './styles';
|
||||||
import {
|
import {
|
||||||
ActionsColumnProps,
|
|
||||||
ColumnTypeRender,
|
ColumnTypeRender,
|
||||||
UseTableViewProps,
|
UseTableViewProps,
|
||||||
UseTableViewResult,
|
UseTableViewResult,
|
||||||
@ -24,60 +23,22 @@ import {
|
|||||||
|
|
||||||
const convert = new Convert();
|
const convert = new Convert();
|
||||||
|
|
||||||
function ActionsColumn({
|
|
||||||
logId,
|
|
||||||
logs,
|
|
||||||
onOpenLogsContext,
|
|
||||||
}: ActionsColumnProps): JSX.Element {
|
|
||||||
const currentLog = useMemo(() => logs.find(({ id }) => id === logId), [
|
|
||||||
logs,
|
|
||||||
logId,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { onLogCopy } = useCopyLogLink(currentLog?.id);
|
|
||||||
|
|
||||||
const handleShowContext = useCallback(() => {
|
|
||||||
if (!onOpenLogsContext || !currentLog) return;
|
|
||||||
|
|
||||||
onOpenLogsContext(currentLog);
|
|
||||||
}, [currentLog, onOpenLogsContext]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={handleShowContext}
|
|
||||||
icon={<MonitorOutlined />}
|
|
||||||
/>
|
|
||||||
<Button size="small" onClick={onLogCopy} icon={<LinkOutlined />} />
|
|
||||||
</Space>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||||
const {
|
const {
|
||||||
logs,
|
logs,
|
||||||
fields,
|
fields,
|
||||||
linesPerRow,
|
linesPerRow,
|
||||||
appendTo = 'center',
|
appendTo = 'center',
|
||||||
onOpenLogsContext,
|
activeContextLog,
|
||||||
onClickExpand,
|
activeLog,
|
||||||
} = props;
|
} = props;
|
||||||
const { isLogsExplorerPage } = useCopyLogLink();
|
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
|
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
|
||||||
logs,
|
logs,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleClickExpand = useCallback(
|
|
||||||
(index: number): void => {
|
|
||||||
if (!onClickExpand) return;
|
|
||||||
|
|
||||||
onClickExpand(logs[index]);
|
|
||||||
},
|
|
||||||
[logs, onClickExpand],
|
|
||||||
);
|
|
||||||
|
|
||||||
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
||||||
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
||||||
.filter((e) => e.name !== 'id')
|
.filter((e) => e.name !== 'id')
|
||||||
@ -87,7 +48,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
key: name,
|
key: name,
|
||||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||||
props: {
|
props: {
|
||||||
style: defaultCellStyle,
|
style: getDefaultCellStyle(isDarkMode),
|
||||||
},
|
},
|
||||||
children: (
|
children: (
|
||||||
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
|
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
|
||||||
@ -98,38 +59,30 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
title: '',
|
|
||||||
dataIndex: 'id',
|
|
||||||
key: 'expand',
|
|
||||||
// https://github.com/ant-design/ant-design/discussions/36886
|
|
||||||
render: (_, item, index): ColumnTypeRender<Record<string, unknown>> => ({
|
|
||||||
props: {
|
|
||||||
style: defaultCellStyle,
|
|
||||||
},
|
|
||||||
children: (
|
|
||||||
<ExpandIconWrapper
|
|
||||||
onClick={(): void => {
|
|
||||||
handleClickExpand(index);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ExpandAltOutlined />
|
|
||||||
</ExpandIconWrapper>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'timestamp',
|
title: 'timestamp',
|
||||||
dataIndex: 'timestamp',
|
dataIndex: 'timestamp',
|
||||||
key: 'timestamp',
|
key: 'timestamp',
|
||||||
// https://github.com/ant-design/ant-design/discussions/36886
|
// https://github.com/ant-design/ant-design/discussions/36886
|
||||||
render: (field): ColumnTypeRender<Record<string, unknown>> => {
|
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
|
||||||
const date =
|
const date =
|
||||||
typeof field === 'string'
|
typeof field === 'string'
|
||||||
? dayjs(field).format()
|
? dayjs(field).format()
|
||||||
: dayjs(field / 1e6).format();
|
: dayjs(field / 1e6).format();
|
||||||
return {
|
return {
|
||||||
children: <Typography.Paragraph ellipsis>{date}</Typography.Paragraph>,
|
children: (
|
||||||
|
<div className="table-timestamp">
|
||||||
|
<LogStateIndicator
|
||||||
|
type={defaultTo(item.log_level, LogType.INFO) as string}
|
||||||
|
isActive={
|
||||||
|
activeLog?.id === item.id || activeContextLog?.id === item.id
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Typography.Paragraph ellipsis className="text">
|
||||||
|
{date}
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -148,38 +101,20 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
|||||||
__html: convert.toHtml(dompurify.sanitize(field)),
|
__html: convert.toHtml(dompurify.sanitize(field)),
|
||||||
}}
|
}}
|
||||||
linesPerRow={linesPerRow}
|
linesPerRow={linesPerRow}
|
||||||
|
isDarkMode={isDarkMode}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
...(appendTo === 'end' ? fieldColumns : []),
|
...(appendTo === 'end' ? fieldColumns : []),
|
||||||
...(isLogsExplorerPage
|
|
||||||
? ([
|
|
||||||
{
|
|
||||||
title: 'actions',
|
|
||||||
dataIndex: 'actions',
|
|
||||||
key: 'actions',
|
|
||||||
render: (_, log): ColumnTypeRender<Record<string, unknown>> => ({
|
|
||||||
children: (
|
|
||||||
<ActionsColumn
|
|
||||||
logId={(log.id as unknown) as string}
|
|
||||||
logs={logs}
|
|
||||||
onOpenLogsContext={onOpenLogsContext}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
] as ColumnsType<Record<string, unknown>>)
|
|
||||||
: []),
|
|
||||||
];
|
];
|
||||||
}, [
|
}, [
|
||||||
logs,
|
|
||||||
fields,
|
fields,
|
||||||
appendTo,
|
appendTo,
|
||||||
|
isDarkMode,
|
||||||
linesPerRow,
|
linesPerRow,
|
||||||
isLogsExplorerPage,
|
activeLog?.id,
|
||||||
handleClickExpand,
|
activeContextLog?.id,
|
||||||
onOpenLogsContext,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return { columns, dataSource: flattenLogData };
|
return { columns, dataSource: flattenLogData };
|
||||||
|
@ -0,0 +1,396 @@
|
|||||||
|
.nested-menu-container {
|
||||||
|
z-index: 2;
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
margin: 6px 0;
|
||||||
|
width: 160px;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
border: 1px solid var(--bg-slate-400, #1d212d);
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(18, 19, 23, 0.8) 0%,
|
||||||
|
rgba(18, 19, 23, 0.9) 98.68%
|
||||||
|
);
|
||||||
|
|
||||||
|
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
|
.menu-container {
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-align: left;
|
||||||
|
color: var(--bg-slate-200, #52575c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-items {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 17px;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
.item-label {
|
||||||
|
display: flex;
|
||||||
|
color: var(--bg-vanilla-400, #c0c1c3);
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-line {
|
||||||
|
height: 1px;
|
||||||
|
background: #1d212d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-lines-per-row {
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--bg-slate-200, #52575c);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px; /* 163.636% */
|
||||||
|
letter-spacing: 0.88px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.lucide {
|
||||||
|
color: var(--bg-vanilla-400, #c0c1c3);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-lines-per-row-input {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.ant-input-number-handler-wrap {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input-number {
|
||||||
|
min-width: 36px;
|
||||||
|
width: auto;
|
||||||
|
border-right: none;
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid var(--bg-slate-400);
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
text-align: center;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input-number-focused {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-input-number-input-wrap {
|
||||||
|
input {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 13px;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.periscope-btn {
|
||||||
|
box-shadow: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
height: 26px;
|
||||||
|
|
||||||
|
border-radius: 0px 1px 1px 0px;
|
||||||
|
background: var(--bg-ink-300, #16181d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-content-container {
|
||||||
|
.add-new-column-header {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--bg-slate-200, #52575c);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px; /* 163.636% */
|
||||||
|
letter-spacing: 0.88px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.lucide {
|
||||||
|
color: var(--bg-vanilla-400, #c0c1c3);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-line {
|
||||||
|
height: 1px;
|
||||||
|
background: #1d212d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content {
|
||||||
|
padding: 12px;
|
||||||
|
|
||||||
|
.column-format,
|
||||||
|
.column-format-new-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 12px;
|
||||||
|
|
||||||
|
.column-name {
|
||||||
|
color: var(--bg-vanilla-400, #c0c1c3);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 13px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
display: none;
|
||||||
|
flex: 0 0 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.delete-btn {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
height: 1rem;
|
||||||
|
width: 0.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-format {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-format-new-options {
|
||||||
|
max-height: 150px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-divider {
|
||||||
|
margin: 12px 0;
|
||||||
|
border-top: 2px solid var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.nested-menu-container {
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
|
||||||
|
.item {
|
||||||
|
.item-label {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-content-container {
|
||||||
|
width: 110%;
|
||||||
|
margin-left: -5%;
|
||||||
|
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(18, 19, 23, 0.8) 0%,
|
||||||
|
rgba(18, 19, 23, 0.9) 98.68%
|
||||||
|
);
|
||||||
|
|
||||||
|
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
|
.column-format {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.nested-menu-container {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(255, 255, 255, 0.8) 0%,
|
||||||
|
rgba(255, 255, 255, 0.9) 98.68%
|
||||||
|
);
|
||||||
|
|
||||||
|
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
.horizontal-line {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content {
|
||||||
|
.column-divider {
|
||||||
|
border-top: 2px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-lines-per-row {
|
||||||
|
.title {
|
||||||
|
color: var(--bg-ink-200);
|
||||||
|
|
||||||
|
.lucide {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-lines-per-row-input {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.periscope-btn {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-container {
|
||||||
|
.title {
|
||||||
|
color: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
.item-label {
|
||||||
|
color: var(--bg-ink-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-content-container {
|
||||||
|
.title {
|
||||||
|
color: var(--bg-ink-200);
|
||||||
|
|
||||||
|
.lucide {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontal-line {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-content {
|
||||||
|
.max-lines-per-row-input {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.periscope-btn {
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column-format,
|
||||||
|
.column-format-new-options {
|
||||||
|
.column-name {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.nested-menu-container {
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
|
||||||
|
.item {
|
||||||
|
.item-label {
|
||||||
|
color: var(--bg-ink-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-item-content-container {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: linear-gradient(
|
||||||
|
139deg,
|
||||||
|
rgba(255, 255, 255, 0.8) 0%,
|
||||||
|
rgba(255, 255, 255, 0.9) 98.68%
|
||||||
|
);
|
||||||
|
|
||||||
|
box-shadow: 4px 10px 16px 2px rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,248 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
|
import './LogsFormatOptionsMenu.styles.scss';
|
||||||
|
|
||||||
|
import { Divider, Input, InputNumber, Tooltip } from 'antd';
|
||||||
|
import cx from 'classnames';
|
||||||
|
import { LogViewMode } from 'container/LogsTable';
|
||||||
|
import { OptionsMenuConfig } from 'container/OptionsMenu/types';
|
||||||
|
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||||
|
import { Check, Minus, Plus, X } from 'lucide-react';
|
||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface LogsFormatOptionsMenuProps {
|
||||||
|
title: string;
|
||||||
|
items: any;
|
||||||
|
selectedOptionFormat: any;
|
||||||
|
config: OptionsMenuConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LogsFormatOptionsMenu({
|
||||||
|
title,
|
||||||
|
items,
|
||||||
|
selectedOptionFormat,
|
||||||
|
config,
|
||||||
|
}: LogsFormatOptionsMenuProps): JSX.Element {
|
||||||
|
const { maxLines, format, addColumn } = config;
|
||||||
|
const [selectedItem, setSelectedItem] = useState(selectedOptionFormat);
|
||||||
|
const maxLinesNumber = (maxLines?.value as number) || 1;
|
||||||
|
const [maxLinesPerRow, setMaxLinesPerRow] = useState<number>(maxLinesNumber);
|
||||||
|
|
||||||
|
const [addNewColumn, setAddNewColumn] = useState(false);
|
||||||
|
|
||||||
|
const onChange = useCallback(
|
||||||
|
(key: LogViewMode) => {
|
||||||
|
if (!format) return;
|
||||||
|
|
||||||
|
format.onChange(key);
|
||||||
|
},
|
||||||
|
[format],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMenuItemClick = (key: LogViewMode): void => {
|
||||||
|
setSelectedItem(key);
|
||||||
|
onChange(key);
|
||||||
|
setAddNewColumn(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const incrementMaxLinesPerRow = (): void => {
|
||||||
|
if (maxLinesPerRow < 10) {
|
||||||
|
setMaxLinesPerRow(maxLinesPerRow + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const decrementMaxLinesPerRow = (): void => {
|
||||||
|
if (maxLinesPerRow > 1) {
|
||||||
|
setMaxLinesPerRow(maxLinesPerRow - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchValueChange = useDebouncedFn((event): void => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const value = event?.target?.value || '';
|
||||||
|
|
||||||
|
if (addColumn && addColumn?.onSearch) {
|
||||||
|
addColumn?.onSearch(value);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
const handleToggleAddNewColumn = (): void => {
|
||||||
|
setAddNewColumn(!addNewColumn);
|
||||||
|
};
|
||||||
|
|
||||||
|
// console.log('optionsMenuConfig', config);
|
||||||
|
|
||||||
|
const handleLinesPerRowChange = (maxLinesPerRow: number | null): void => {
|
||||||
|
if (
|
||||||
|
maxLinesPerRow &&
|
||||||
|
Number.isInteger(maxLinesNumber) &&
|
||||||
|
maxLinesPerRow > 1
|
||||||
|
) {
|
||||||
|
setMaxLinesPerRow(maxLinesPerRow);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (maxLinesPerRow && config && config.maxLines?.onChange) {
|
||||||
|
config.maxLines.onChange(maxLinesPerRow);
|
||||||
|
}
|
||||||
|
}, [maxLinesPerRow]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx('nested-menu-container', addNewColumn ? 'active' : '')}
|
||||||
|
onClick={(event): void => {
|
||||||
|
// this is to restrict click events to propogate to parent
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="menu-container">
|
||||||
|
<div className="title"> {title} </div>
|
||||||
|
|
||||||
|
<div className="menu-items">
|
||||||
|
{items.map(
|
||||||
|
(item: any): JSX.Element => (
|
||||||
|
<div
|
||||||
|
className="item"
|
||||||
|
key={item.label}
|
||||||
|
onClick={(): void => handleMenuItemClick(item.key)}
|
||||||
|
>
|
||||||
|
<div className={cx('item-label')}>
|
||||||
|
{item.label}
|
||||||
|
|
||||||
|
{selectedItem === item.key && <Check size={12} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedItem && (
|
||||||
|
<>
|
||||||
|
{selectedItem === 'raw' && (
|
||||||
|
<>
|
||||||
|
<div className="horizontal-line" />
|
||||||
|
<div className="max-lines-per-row">
|
||||||
|
<div className="title"> max lines per row </div>
|
||||||
|
<div className="raw-format max-lines-per-row-input">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="periscope-btn"
|
||||||
|
onClick={decrementMaxLinesPerRow}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
<Minus size={12} />{' '}
|
||||||
|
</button>
|
||||||
|
<InputNumber
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
value={maxLinesPerRow}
|
||||||
|
onChange={handleLinesPerRowChange}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="periscope-btn"
|
||||||
|
onClick={incrementMaxLinesPerRow}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
<Plus size={12} />{' '}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="selected-item-content-container active">
|
||||||
|
{!addNewColumn && <div className="horizontal-line" />}
|
||||||
|
|
||||||
|
{addNewColumn && (
|
||||||
|
<div className="add-new-column-header">
|
||||||
|
<div className="title">
|
||||||
|
{' '}
|
||||||
|
columns
|
||||||
|
<X size={14} onClick={handleToggleAddNewColumn} />{' '}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
tabIndex={0}
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
onFocus={addColumn?.onFocus}
|
||||||
|
onChange={handleSearchValueChange}
|
||||||
|
placeholder="Search..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="item-content">
|
||||||
|
{!addNewColumn && (
|
||||||
|
<div className="title">
|
||||||
|
columns
|
||||||
|
<Plus size={14} onClick={handleToggleAddNewColumn} />{' '}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="column-format">
|
||||||
|
{addColumn?.value?.map(({ key, id }) => (
|
||||||
|
<div className="column-name" key={id}>
|
||||||
|
<div className="name">
|
||||||
|
<Tooltip placement="left" title={key}>
|
||||||
|
{key}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<X
|
||||||
|
className="delete-btn"
|
||||||
|
size={14}
|
||||||
|
onClick={(): void => addColumn.onRemove(id as string)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{addColumn?.isFetching && (
|
||||||
|
<div className="loading-container"> Loading ... </div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{addNewColumn &&
|
||||||
|
addColumn &&
|
||||||
|
addColumn.value.length > 0 &&
|
||||||
|
addColumn.options &&
|
||||||
|
addColumn?.options?.length > 0 && (
|
||||||
|
<Divider className="column-divider" />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{addNewColumn && (
|
||||||
|
<div className="column-format-new-options">
|
||||||
|
{addColumn?.options?.map(({ label, value }) => (
|
||||||
|
<div
|
||||||
|
className="column-name"
|
||||||
|
key={value}
|
||||||
|
onClick={(eve): void => {
|
||||||
|
console.log('coluimn name', label, value);
|
||||||
|
|
||||||
|
eve.stopPropagation();
|
||||||
|
|
||||||
|
if (addColumn && addColumn?.onSelect) {
|
||||||
|
addColumn?.onSelect(value, { label, disabled: false });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="name">
|
||||||
|
<Tooltip placement="left" title={label}>
|
||||||
|
{label}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -25,11 +25,12 @@ const allComponentMap: ComponentMapType[] = [
|
|||||||
if (!path) {
|
if (!path) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const allowedPaths = [
|
const allowedPaths: string[] = [
|
||||||
ROUTES.LIST_ALL_ALERT,
|
ROUTES.LIST_ALL_ALERT,
|
||||||
ROUTES.APPLICATION,
|
ROUTES.APPLICATION,
|
||||||
ROUTES.ALL_DASHBOARD,
|
ROUTES.ALL_DASHBOARD,
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
userFlags?.ReleaseNote0120Hide !== 'Y' &&
|
userFlags?.ReleaseNote0120Hide !== 'Y' &&
|
||||||
allowedPaths.includes(path) &&
|
allowedPaths.includes(path) &&
|
||||||
|
@ -1,25 +1,31 @@
|
|||||||
.DynamicColumnTable {
|
.DynamicColumnTable {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.dynamicColumnTable-button {
|
.dynamicColumnTable-button {
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
|
||||||
|
&.filter-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dynamicColumnsTable-items {
|
.dynamicColumnsTable-items {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 10.625rem;
|
width: 10.625rem;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.dynamicColumnsTable-items {
|
.dynamicColumnsTable-items {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: auto;
|
width: auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
/* eslint-disable react/jsx-props-no-spreading */
|
/* eslint-disable react/jsx-props-no-spreading */
|
||||||
import './DynamicColumnTable.syles.scss';
|
import './DynamicColumnTable.syles.scss';
|
||||||
|
|
||||||
import { SettingOutlined } from '@ant-design/icons';
|
|
||||||
import { Button, Dropdown, MenuProps, Switch } from 'antd';
|
import { Button, Dropdown, MenuProps, Switch } from 'antd';
|
||||||
import { ColumnsType } from 'antd/lib/table';
|
import { ColumnsType } from 'antd/lib/table';
|
||||||
|
import { SlidersHorizontal } from 'lucide-react';
|
||||||
import { memo, useEffect, useState } from 'react';
|
import { memo, useEffect, useState } from 'react';
|
||||||
import { popupContainer } from 'utils/selectPopupContainer';
|
import { popupContainer } from 'utils/selectPopupContainer';
|
||||||
|
|
||||||
@ -90,9 +90,9 @@ function DynamicColumnTable({
|
|||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="dynamicColumnTable-button"
|
className="dynamicColumnTable-button filter-btn"
|
||||||
size="middle"
|
size="middle"
|
||||||
icon={<SettingOutlined />}
|
icon={<SlidersHorizontal size={14} />}
|
||||||
/>
|
/>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { TabsProps } from 'antd';
|
import { TabsProps } from 'antd';
|
||||||
import { History } from 'history';
|
import { History } from 'history';
|
||||||
|
|
||||||
|
export type TabRoutes = {
|
||||||
|
name: React.ReactNode;
|
||||||
|
route: string;
|
||||||
|
Component: () => JSX.Element;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface RouteTabProps {
|
export interface RouteTabProps {
|
||||||
routes: {
|
routes: TabRoutes[];
|
||||||
name: React.ReactNode;
|
|
||||||
route: string;
|
|
||||||
Component: () => JSX.Element;
|
|
||||||
key: string;
|
|
||||||
}[];
|
|
||||||
activeKey: TabsProps['activeKey'];
|
activeKey: TabsProps['activeKey'];
|
||||||
onChangeHandler?: VoidFunction;
|
onChangeHandler?: VoidFunction;
|
||||||
history: History<unknown>;
|
history: History<unknown>;
|
||||||
|
@ -15,4 +15,5 @@ export enum LOCALSTORAGE {
|
|||||||
LOGGED_IN_USER_EMAIL = 'LOGGED_IN_USER_EMAIL',
|
LOGGED_IN_USER_EMAIL = 'LOGGED_IN_USER_EMAIL',
|
||||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||||
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
|
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
|
||||||
|
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,10 @@ const ROUTES = {
|
|||||||
TRACE_EXPLORER: '/trace-explorer',
|
TRACE_EXPLORER: '/trace-explorer',
|
||||||
BILLING: '/billing',
|
BILLING: '/billing',
|
||||||
SUPPORT: '/support',
|
SUPPORT: '/support',
|
||||||
|
LOGS_SAVE_VIEWS: '/logs-save-views',
|
||||||
|
TRACES_SAVE_VIEWS: '/traces-save-views',
|
||||||
WORKSPACE_LOCKED: '/workspace-locked',
|
WORKSPACE_LOCKED: '/workspace-locked',
|
||||||
};
|
SHORTCUTS: '/shortcuts',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export default ROUTES;
|
export default ROUTES;
|
||||||
|
32
frontend/src/constants/shortcuts/globalShortcuts.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||||
|
|
||||||
|
const userOS = getUserOperatingSystem();
|
||||||
|
export const GlobalShortcuts = {
|
||||||
|
SidebarCollapse: '\\+meta',
|
||||||
|
NavigateToServices: 's+shift',
|
||||||
|
NavigateToTraces: 't+shift',
|
||||||
|
NavigateToLogs: 'l+shift',
|
||||||
|
NavigateToDashboards: 'd+shift',
|
||||||
|
NavigateToAlerts: 'a+shift',
|
||||||
|
NavigateToExceptions: 'e+shift',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GlobalShortcutsName = {
|
||||||
|
SidebarCollapse: `${userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'}+\\`,
|
||||||
|
NavigateToServices: 'shift+s',
|
||||||
|
NavigateToTraces: 'shift+t',
|
||||||
|
NavigateToLogs: 'shift+l',
|
||||||
|
NavigateToDashboards: 'shift+d',
|
||||||
|
NavigateToAlerts: 'shift+a',
|
||||||
|
NavigateToExceptions: 'shift+e',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GlobalShortcutsDescription = {
|
||||||
|
SidebarCollapse: 'Collpase the sidebar',
|
||||||
|
NavigateToServices: 'Navigate to Services page',
|
||||||
|
NavigateToTraces: 'Navigate to Traces page',
|
||||||
|
NavigateToLogs: 'Navigate to logs page',
|
||||||
|
NavigateToDashboards: 'Navigate to dashboards page',
|
||||||
|
NavigateToAlerts: 'Navigate to alerts page',
|
||||||
|
NavigateToExceptions: 'Navigate to Exceptions page',
|
||||||
|
};
|
19
frontend/src/constants/shortcuts/logsExplorerShortcuts.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||||
|
|
||||||
|
const userOS = getUserOperatingSystem();
|
||||||
|
export const LogsExplorerShortcuts = {
|
||||||
|
StageAndRunQuery: 'enter+meta',
|
||||||
|
FocusTheSearchBar: 's',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LogsExplorerShortcutsName = {
|
||||||
|
StageAndRunQuery: `${
|
||||||
|
userOS === UserOperatingSystem.MACOS ? 'cmd' : 'ctrl'
|
||||||
|
}+enter`,
|
||||||
|
FocusTheSearchBar: 's',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LogsExplorerShortcutsDescription = {
|
||||||
|
StageAndRunQuery: 'Stage and Run the current query',
|
||||||
|
FocusTheSearchBar: 'Shift the focus to the last query filter bar',
|
||||||
|
};
|
@ -64,7 +64,9 @@ function AlertChannels({ allChannels }: AlertChannelsProps): JSX.Element {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return <ResizeTable columns={columns} dataSource={channels} rowKey="id" />;
|
return (
|
||||||
|
<ResizeTable columns={columns} dataSource={channels} rowKey="id" bordered />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AlertChannelsProps {
|
interface AlertChannelsProps {
|
||||||
|
@ -1,12 +1,20 @@
|
|||||||
@import '@signozhq/design-tokens';
|
|
||||||
|
|
||||||
.app-layout {
|
.app-layout {
|
||||||
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,10 +4,12 @@
|
|||||||
import './AppLayout.styles.scss';
|
import './AppLayout.styles.scss';
|
||||||
|
|
||||||
import { Flex } from 'antd';
|
import { Flex } from 'antd';
|
||||||
|
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||||
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
|
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
|
||||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||||
import getUserVersion from 'api/user/getVersion';
|
import getUserVersion from 'api/user/getVersion';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
|
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
|
||||||
import ROUTES from 'constants/routes';
|
import ROUTES from 'constants/routes';
|
||||||
import SideNav from 'container/SideNav';
|
import SideNav from 'container/SideNav';
|
||||||
import TopNav from 'container/TopNav';
|
import TopNav from 'container/TopNav';
|
||||||
@ -16,7 +18,15 @@ import useLicense from 'hooks/useLicense';
|
|||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import history from 'lib/history';
|
import history from 'lib/history';
|
||||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||||
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
|
import {
|
||||||
|
ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { Helmet } from 'react-helmet-async';
|
import { Helmet } from 'react-helmet-async';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -24,6 +34,7 @@ import { useQueries } from 'react-query';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
import { Dispatch } from 'redux';
|
import { Dispatch } from 'redux';
|
||||||
|
import { sideBarCollapse } from 'store/actions';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
import AppActions from 'types/actions';
|
import AppActions from 'types/actions';
|
||||||
import {
|
import {
|
||||||
@ -44,6 +55,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [collapsed, setCollapsed] = useState<boolean>(
|
||||||
|
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
|
||||||
|
);
|
||||||
|
|
||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
const { data: licenseData, isFetching } = useLicense();
|
const { data: licenseData, isFetching } = useLicense();
|
||||||
@ -92,7 +107,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
|
|
||||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
const dispatch = useDispatch<Dispatch<AppActions | any>>();
|
||||||
|
|
||||||
const latestCurrentCounter = useRef(0);
|
const latestCurrentCounter = useRef(0);
|
||||||
const latestVersionCounter = useRef(0);
|
const latestVersionCounter = useRef(0);
|
||||||
@ -100,6 +115,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
const { notifications } = useNotifications();
|
const { notifications } = useNotifications();
|
||||||
|
|
||||||
|
const onCollapse = useCallback(() => {
|
||||||
|
setCollapsed((collapsed) => !collapsed);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
dispatch(sideBarCollapse(collapsed));
|
||||||
|
}, [collapsed, dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
getUserLatestVersionResponse.isFetched &&
|
getUserLatestVersionResponse.isFetched &&
|
||||||
@ -230,8 +253,34 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isLogsView = (): boolean =>
|
||||||
|
routeKey === 'LOGS' ||
|
||||||
|
routeKey === 'LOGS_EXPLORER' ||
|
||||||
|
routeKey === 'LOGS_PIPELINES' ||
|
||||||
|
routeKey === 'LOGS_SAVE_VIEWS';
|
||||||
|
|
||||||
|
const isTracesView = (): boolean =>
|
||||||
|
routeKey === 'TRACES_EXPLORER' || routeKey === 'TRACES_SAVE_VIEWS';
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDarkMode) {
|
||||||
|
document.body.classList.remove('lightMode');
|
||||||
|
document.body.classList.add('darkMode');
|
||||||
|
} else {
|
||||||
|
document.body.classList.add('lightMode');
|
||||||
|
document.body.classList.remove('darkMode');
|
||||||
|
}
|
||||||
|
}, [isDarkMode]);
|
||||||
|
|
||||||
|
const isSideNavCollapsed = getLocalStorageKey(IS_SIDEBAR_COLLAPSED);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className={isDarkMode ? 'darkMode' : 'lightMode'}>
|
<Layout
|
||||||
|
className={cx(
|
||||||
|
isDarkMode ? 'darkMode' : 'lightMode',
|
||||||
|
isSideNavCollapsed ? 'sidebarCollapsed' : '',
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{pageTitle}</title>
|
<title>{pageTitle}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
@ -259,12 +308,21 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
|||||||
|
|
||||||
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
|
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||||
{isToDisplayLayout && !renderFullScreen && (
|
{isToDisplayLayout && !renderFullScreen && (
|
||||||
<SideNav licenseData={licenseData} isFetching={isFetching} />
|
<SideNav
|
||||||
|
licenseData={licenseData}
|
||||||
|
isFetching={isFetching}
|
||||||
|
onCollapse={onCollapse}
|
||||||
|
collapsed={collapsed}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="app-content">
|
<div className={cx('app-content', collapsed ? 'collapsed' : '')}>
|
||||||
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
<ErrorBoundary FallbackComponent={ErrorBoundaryFallback}>
|
||||||
<LayoutContent>
|
<LayoutContent>
|
||||||
<ChildrenContainer>
|
<ChildrenContainer
|
||||||
|
style={{
|
||||||
|
margin: isLogsView() || isTracesView() ? 0 : ' 0 1rem',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||||
{children}
|
{children}
|
||||||
</ChildrenContainer>
|
</ChildrenContainer>
|
||||||
|
@ -18,7 +18,6 @@ export const LayoutContent = styled(LayoutComponent.Content)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const ChildrenContainer = styled.div`
|
export const ChildrenContainer = styled.div`
|
||||||
margin: 0 1rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -7,16 +7,18 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
|||||||
const [formInstance] = Form.useForm();
|
const [formInstance] = Form.useForm();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormAlertRules
|
<div style={{ marginTop: '1rem' }}>
|
||||||
alertType={
|
<FormAlertRules
|
||||||
initialValue.alertType
|
alertType={
|
||||||
? (initialValue.alertType as AlertTypes)
|
initialValue.alertType
|
||||||
: AlertTypes.METRICS_BASED_ALERT
|
? (initialValue.alertType as AlertTypes)
|
||||||
}
|
: AlertTypes.METRICS_BASED_ALERT
|
||||||
formInstance={formInstance}
|
}
|
||||||
initialValue={initialValue}
|
formInstance={formInstance}
|
||||||
ruleId={ruleId}
|
initialValue={initialValue}
|
||||||
/>
|
ruleId={ruleId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
.empty-logs-search-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 240px;
|
||||||
|
|
||||||
|
.empty-logs-search-container-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
color: var(--text-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px; /* 128.571% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
|
||||||
|
.empty-state-svg {
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-text {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
frontend/src/container/EmptyLogsSearch/EmptyLogsSearch.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import './EmptyLogsSearch.styles.scss';
|
||||||
|
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
|
||||||
|
export default function EmptyLogsSearch(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="empty-logs-search-container">
|
||||||
|
<div className="empty-logs-search-container-content">
|
||||||
|
<img
|
||||||
|
src="/Icons/emptyState.svg"
|
||||||
|
alt="thinking-emoji"
|
||||||
|
className="empty-state-svg"
|
||||||
|
/>
|
||||||
|
<Typography.Text>
|
||||||
|
<span className="sub-text">This query had no results. </span>
|
||||||
|
Edit your query and try again!
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,307 @@
|
|||||||
|
.explorer-update {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
left: calc(50% - 225px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: rgba(22, 24, 29, 0.6);
|
||||||
|
box-shadow: 4px 4px 16px 4px rgba(0, 0, 0, 0.25);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-divider {
|
||||||
|
margin: 0;
|
||||||
|
height: 28px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.explorer-options {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 50px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: rgba(22, 24, 29, 0.6);
|
||||||
|
box-shadow: 4px 4px 16px 4px rgba(0, 0, 0, 0.25);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
position: fixed;
|
||||||
|
bottom: 16px;
|
||||||
|
left: calc(50% + 240px);
|
||||||
|
transform: translate(calc(-50% - 120px), 0);
|
||||||
|
transition: left 0.2s linear;
|
||||||
|
|
||||||
|
.ant-select-selector {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-color: #1d212d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-options,
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
border: 1px solid #1d2023;
|
||||||
|
color: #c0c1c3;
|
||||||
|
background-color: #161922;
|
||||||
|
|
||||||
|
box-shadow: none !important;
|
||||||
|
|
||||||
|
&.ant-btn-round {
|
||||||
|
padding-inline-start: 10px;
|
||||||
|
padding-inline-end: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-btn-round:disabled {
|
||||||
|
background-color: rgba(209, 209, 209, 0.074);
|
||||||
|
color: #5f5f5f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-focused {
|
||||||
|
border-color: transparent !important;
|
||||||
|
|
||||||
|
.ant-select-selector {
|
||||||
|
border-color: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-selector {
|
||||||
|
border: transparent !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
|
||||||
|
.ant-select-selection-placeholder {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-content {
|
||||||
|
&.collapsed {
|
||||||
|
.explorer-options {
|
||||||
|
left: calc(50% + 72px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.render-options {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 0 2px;
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
letter-spacing: -0.07px;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
margin-left: 6px;
|
||||||
|
min-height: 6px;
|
||||||
|
min-width: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-view-modal {
|
||||||
|
width: 384px !important;
|
||||||
|
.ant-modal-content {
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--bg-slate-500);
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
box-shadow: 0px -4px 16px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
.ant-modal-header {
|
||||||
|
padding: 16px;
|
||||||
|
background: var(--bg-ink-400);
|
||||||
|
border-bottom: 1px solid var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
padding: 12px 16px 0px 16px;
|
||||||
|
|
||||||
|
.ant-typography {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 20px; /* 142.857% */
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-view-input {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-color-picker-trigger {
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
background: var(--bg-ink-300);
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
.ant-color-picker-color-block {
|
||||||
|
border-radius: 50px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.ant-color-picker-color-block-inner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 16px 16px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
background-color: var(--bg-robin-500) !important;
|
||||||
|
color: var(--bg-vanilla-100) !important;
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
color: var(--bg-vanilla-100);
|
||||||
|
font-family: Inter;
|
||||||
|
font-size: 14px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.explorer-options {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
box-shadow: 4px 4px 16px 4px rgba(255, 255, 255, 0.55);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
|
hr {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-options,
|
||||||
|
.actions {
|
||||||
|
button {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
color: var(--bg-ink-200);
|
||||||
|
background-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.render-options {
|
||||||
|
color: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.explorer-update {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: 4px 4px 16px 4px rgba(255, 255, 255, 0.55);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
|
||||||
|
.action-icon {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-divider {
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tooltip-arrow {
|
||||||
|
border-top-color: var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tooltip-inner {
|
||||||
|
background-color: var(--bg-vanilla-300);
|
||||||
|
color: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-view-modal {
|
||||||
|
.ant-modal-content {
|
||||||
|
background: var(--bg-vanilla-200);
|
||||||
|
border-color: var(--bg-vanilla-300);
|
||||||
|
|
||||||
|
.ant-modal-header {
|
||||||
|
background: var(--bg-vanilla-200);
|
||||||
|
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-body {
|
||||||
|
.ant-typography {
|
||||||
|
color: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-color-picker-trigger {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
background: var(--bg-vanilla-100);
|
||||||
|
|
||||||
|
.ant-color-picker-color-block {
|
||||||
|
.ant-color-picker-color-block-inner {
|
||||||
|
svg {
|
||||||
|
fill: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: var(--bg-ink-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
413
frontend/src/container/ExplorerOptions/ExplorerOptions.tsx
Normal file
@ -0,0 +1,413 @@
|
|||||||
|
import './ExplorerOptions.styles.scss';
|
||||||
|
|
||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
ColorPicker,
|
||||||
|
Divider,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
RefSelectProps,
|
||||||
|
Select,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { getViewDetailsUsingViewKey } from 'components/ExplorerCard/utils';
|
||||||
|
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import ExportPanelContainer from 'container/ExportPanel/ExportPanelContainer';
|
||||||
|
import { useGetSearchQueryParam } from 'hooks/queryBuilder/useGetSearchQueryParam';
|
||||||
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||||
|
import { useGetAllViews } from 'hooks/saveViews/useGetAllViews';
|
||||||
|
import { useSaveView } from 'hooks/saveViews/useSaveView';
|
||||||
|
import { useUpdateView } from 'hooks/saveViews/useUpdateView';
|
||||||
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
|
import useErrorNotification from 'hooks/useErrorNotification';
|
||||||
|
import { useHandleExplorerTabChange } from 'hooks/useHandleExplorerTabChange';
|
||||||
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
|
import { mapCompositeQueryFromQuery } from 'lib/newQueryBuilder/queryBuilderMappers/mapCompositeQueryFromQuery';
|
||||||
|
import { Check, ConciergeBell, Disc3, Plus, X, XCircle } from 'lucide-react';
|
||||||
|
import { CSSProperties, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DATASOURCE_VS_ROUTES,
|
||||||
|
generateRGBAFromHex,
|
||||||
|
getRandomColor,
|
||||||
|
saveNewViewHandler,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
function ExplorerOptions({
|
||||||
|
disabled,
|
||||||
|
isLoading,
|
||||||
|
onExport,
|
||||||
|
query,
|
||||||
|
sourcepage,
|
||||||
|
}: ExplorerOptionsProps): JSX.Element {
|
||||||
|
const [isExport, setIsExport] = useState<boolean>(false);
|
||||||
|
const [isSaveModalOpen, setIsSaveModalOpen] = useState(false);
|
||||||
|
const [newViewName, setNewViewName] = useState<string>('');
|
||||||
|
const [color, setColor] = useState(Color.BG_SIENNA_500);
|
||||||
|
const { notifications } = useNotifications();
|
||||||
|
const history = useHistory();
|
||||||
|
const ref = useRef<RefSelectProps>(null);
|
||||||
|
const isDarkMode = useIsDarkMode();
|
||||||
|
|
||||||
|
const onModalToggle = useCallback((value: boolean) => {
|
||||||
|
setIsExport(value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSaveViewModalToggle = (): void => {
|
||||||
|
setIsSaveModalOpen(!isSaveModalOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideSaveViewModal = (): void => {
|
||||||
|
setIsSaveModalOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreateAlertsHandler = useCallback(() => {
|
||||||
|
history.push(
|
||||||
|
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||||
|
JSON.stringify(query),
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}, [history, query]);
|
||||||
|
|
||||||
|
const onCancel = (value: boolean) => (): void => {
|
||||||
|
onModalToggle(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddToDashboard = (): void => {
|
||||||
|
setIsExport(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: viewsData,
|
||||||
|
isLoading: viewsIsLoading,
|
||||||
|
error,
|
||||||
|
isRefetching,
|
||||||
|
refetch: refetchAllView,
|
||||||
|
} = useGetAllViews(sourcepage);
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentQuery,
|
||||||
|
panelType,
|
||||||
|
isStagedQueryUpdated,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
} = useQueryBuilder();
|
||||||
|
|
||||||
|
const compositeQuery = mapCompositeQueryFromQuery(currentQuery, panelType);
|
||||||
|
|
||||||
|
const viewName = useGetSearchQueryParam(QueryParams.viewName) || '';
|
||||||
|
const viewKey = useGetSearchQueryParam(QueryParams.viewKey) || '';
|
||||||
|
|
||||||
|
const extraData = viewsData?.data?.data?.find((view) => view.uuid === viewKey)
|
||||||
|
?.extraData;
|
||||||
|
|
||||||
|
const extraDataColor = extraData ? JSON.parse(extraData).color : '';
|
||||||
|
const rgbaColor = generateRGBAFromHex(
|
||||||
|
extraDataColor || Color.BG_SIENNA_500,
|
||||||
|
0.08,
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync: updateViewAsync,
|
||||||
|
isLoading: isViewUpdating,
|
||||||
|
} = useUpdateView({
|
||||||
|
compositeQuery,
|
||||||
|
viewKey,
|
||||||
|
extraData: extraData || JSON.stringify({ color: Color.BG_SIENNA_500 }),
|
||||||
|
sourcePage: sourcepage,
|
||||||
|
viewName,
|
||||||
|
});
|
||||||
|
|
||||||
|
const showErrorNotification = (err: Error): void => {
|
||||||
|
notifications.error({
|
||||||
|
message: axios.isAxiosError(err) ? err.message : SOMETHING_WENT_WRONG,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUpdateQueryHandler = (): void => {
|
||||||
|
const extraData = viewsData?.data?.data?.find((view) => view.uuid === viewKey)
|
||||||
|
?.extraData;
|
||||||
|
updateViewAsync(
|
||||||
|
{
|
||||||
|
compositeQuery: mapCompositeQueryFromQuery(currentQuery, panelType),
|
||||||
|
viewKey,
|
||||||
|
extraData: extraData || JSON.stringify({ color: Color.BG_SIENNA_500 }),
|
||||||
|
sourcePage: sourcepage,
|
||||||
|
viewName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
notifications.success({
|
||||||
|
message: 'View Updated Successfully',
|
||||||
|
});
|
||||||
|
refetchAllView();
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
showErrorNotification(err);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useErrorNotification(error);
|
||||||
|
|
||||||
|
const { handleExplorerTabChange } = useHandleExplorerTabChange();
|
||||||
|
|
||||||
|
const onMenuItemSelectHandler = useCallback(
|
||||||
|
({ key }: { key: string }): void => {
|
||||||
|
const currentViewDetails = getViewDetailsUsingViewKey(
|
||||||
|
key,
|
||||||
|
viewsData?.data?.data,
|
||||||
|
);
|
||||||
|
if (!currentViewDetails) return;
|
||||||
|
const {
|
||||||
|
query,
|
||||||
|
name,
|
||||||
|
uuid,
|
||||||
|
panelType: currentPanelType,
|
||||||
|
} = currentViewDetails;
|
||||||
|
|
||||||
|
handleExplorerTabChange(currentPanelType, {
|
||||||
|
query,
|
||||||
|
name,
|
||||||
|
uuid,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[viewsData, handleExplorerTabChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelect = (
|
||||||
|
value: string,
|
||||||
|
option: { key: string; value: string },
|
||||||
|
): void => {
|
||||||
|
onMenuItemSelectHandler({
|
||||||
|
key: option.key,
|
||||||
|
});
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.blur();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearSelect = (): void => {
|
||||||
|
history.replace(DATASOURCE_VS_ROUTES[sourcepage]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isQueryUpdated = isStagedQueryUpdated(viewsData?.data?.data, viewKey);
|
||||||
|
|
||||||
|
const {
|
||||||
|
isLoading: isSaveViewLoading,
|
||||||
|
mutateAsync: saveViewAsync,
|
||||||
|
} = useSaveView({
|
||||||
|
viewName: newViewName || '',
|
||||||
|
compositeQuery,
|
||||||
|
sourcePage: sourcepage,
|
||||||
|
extraData: JSON.stringify({ color }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSaveHandler = (): void => {
|
||||||
|
saveNewViewHandler({
|
||||||
|
compositeQuery,
|
||||||
|
handlePopOverClose: hideSaveViewModal,
|
||||||
|
extraData: JSON.stringify({ color }),
|
||||||
|
notifications,
|
||||||
|
panelType: panelType || PANEL_TYPES.LIST,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
refetchAllView,
|
||||||
|
saveViewAsync,
|
||||||
|
sourcePage: sourcepage,
|
||||||
|
viewName: newViewName,
|
||||||
|
setNewViewName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Remove this and move this to scss file
|
||||||
|
const dropdownStyle: CSSProperties = useMemo(
|
||||||
|
() => ({
|
||||||
|
borderRadius: '4px',
|
||||||
|
border: isDarkMode
|
||||||
|
? `1px solid ${Color.BG_SLATE_400}`
|
||||||
|
: `1px solid ${Color.BG_VANILLA_300}`,
|
||||||
|
background: isDarkMode
|
||||||
|
? 'linear-gradient(139deg, rgba(18, 19, 23, 0.80) 0%, rgba(18, 19, 23, 0.90) 98.68%)'
|
||||||
|
: 'linear-gradient(139deg, rgba(241, 241, 241, 0.8) 0%, rgba(241, 241, 241, 0.9) 98.68%)',
|
||||||
|
boxShadow: '4px 10px 16px 2px rgba(0, 0, 0, 0.20)',
|
||||||
|
backdropFilter: 'blur(20px)',
|
||||||
|
bottom: '74px',
|
||||||
|
width: '191px',
|
||||||
|
}),
|
||||||
|
[isDarkMode],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isQueryUpdated && (
|
||||||
|
<div className="explorer-update">
|
||||||
|
<Tooltip title="Clear this view" placement="top">
|
||||||
|
<Button
|
||||||
|
className="action-icon"
|
||||||
|
onClick={handleClearSelect}
|
||||||
|
icon={<X size={14} />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Divider type="vertical" />
|
||||||
|
<Tooltip title="Update this view" placement="top">
|
||||||
|
<Button
|
||||||
|
className="action-icon"
|
||||||
|
disabled={isViewUpdating}
|
||||||
|
onClick={onUpdateQueryHandler}
|
||||||
|
icon={<Disc3 size={14} />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="explorer-options"
|
||||||
|
style={{
|
||||||
|
background: extraData
|
||||||
|
? `linear-gradient(90deg, rgba(0,0,0,0) -5%, ${rgbaColor} 9%, rgba(0,0,0,0) 30%)`
|
||||||
|
: 'transparent',
|
||||||
|
backdropFilter: 'blur(20px)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="view-options">
|
||||||
|
<Select<string, { key: string; value: string }>
|
||||||
|
showSearch
|
||||||
|
placeholder="Select a view"
|
||||||
|
loading={viewsIsLoading || isRefetching}
|
||||||
|
value={viewName || undefined}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
style={{
|
||||||
|
minWidth: 170,
|
||||||
|
}}
|
||||||
|
dropdownStyle={dropdownStyle}
|
||||||
|
className="views-dropdown"
|
||||||
|
allowClear={{
|
||||||
|
clearIcon: <XCircle size={16} style={{ marginTop: '-3px' }} />,
|
||||||
|
}}
|
||||||
|
onClear={handleClearSelect}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{viewsData?.data?.data?.map((view) => {
|
||||||
|
const extraData =
|
||||||
|
view.extraData !== '' ? JSON.parse(view.extraData) : '';
|
||||||
|
let bgColor = getRandomColor();
|
||||||
|
if (extraData !== '') {
|
||||||
|
bgColor = extraData.color;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Select.Option key={view.uuid} value={view.name}>
|
||||||
|
<div className="render-options">
|
||||||
|
<span
|
||||||
|
className="dot"
|
||||||
|
style={{
|
||||||
|
background: bgColor,
|
||||||
|
boxShadow: `0px 0px 6px 0px ${bgColor}`,
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
{view.name}
|
||||||
|
</div>
|
||||||
|
</Select.Option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
shape="round"
|
||||||
|
onClick={handleSaveViewModalToggle}
|
||||||
|
disabled={viewsIsLoading || isRefetching}
|
||||||
|
>
|
||||||
|
<Disc3 size={16} /> Save this view
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div className="actions">
|
||||||
|
<Tooltip title="Create Alerts">
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
shape="circle"
|
||||||
|
onClick={onCreateAlertsHandler}
|
||||||
|
>
|
||||||
|
<ConciergeBell size={16} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title="Add to Dashboard">
|
||||||
|
<Button disabled={disabled} shape="circle" onClick={onAddToDashboard}>
|
||||||
|
<Plus size={16} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
className="save-view-modal"
|
||||||
|
title={<span className="title">Save this view</span>}
|
||||||
|
open={isSaveModalOpen}
|
||||||
|
closable
|
||||||
|
onCancel={hideSaveViewModal}
|
||||||
|
footer={[
|
||||||
|
<Button
|
||||||
|
key="submit"
|
||||||
|
type="primary"
|
||||||
|
icon={<Check size={16} />}
|
||||||
|
onClick={onSaveHandler}
|
||||||
|
disabled={isSaveViewLoading}
|
||||||
|
>
|
||||||
|
Save this view
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Typography.Text>Label</Typography.Text>
|
||||||
|
<div className="save-view-input">
|
||||||
|
<ColorPicker
|
||||||
|
value={color}
|
||||||
|
onChange={(value, hex): void => setColor(hex)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="e.g. External http method view"
|
||||||
|
value={newViewName}
|
||||||
|
onChange={(e): void => setNewViewName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
footer={null}
|
||||||
|
onOk={onCancel(false)}
|
||||||
|
onCancel={onCancel(false)}
|
||||||
|
open={isExport}
|
||||||
|
centered
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<ExportPanelContainer
|
||||||
|
query={query}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onExport={onExport}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExplorerOptionsProps {
|
||||||
|
isLoading?: boolean;
|
||||||
|
onExport: (dashboard: Dashboard | null) => void;
|
||||||
|
query: Query | null;
|
||||||
|
disabled: boolean;
|
||||||
|
sourcepage: DataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExplorerOptions.defaultProps = { isLoading: false };
|
||||||
|
|
||||||
|
export default ExplorerOptions;
|
28
frontend/src/container/ExplorerOptions/types.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { NotificationInstance } from 'antd/es/notification/interface';
|
||||||
|
import { AxiosResponse } from 'axios';
|
||||||
|
import { SaveViewWithNameProps } from 'components/ExplorerCard/types';
|
||||||
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { UseMutateAsyncFunction } from 'react-query';
|
||||||
|
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||||
|
import { SaveViewPayloadProps, SaveViewProps } from 'types/api/saveViews/types';
|
||||||
|
import { DataSource, QueryBuilderContextType } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
export interface SaveNewViewHandlerProps {
|
||||||
|
viewName: string;
|
||||||
|
compositeQuery: ICompositeMetricQuery;
|
||||||
|
sourcePage: DataSource;
|
||||||
|
extraData: SaveViewProps['extraData'];
|
||||||
|
panelType: PANEL_TYPES | null;
|
||||||
|
notifications: NotificationInstance;
|
||||||
|
refetchAllView: SaveViewWithNameProps['refetchAllView'];
|
||||||
|
saveViewAsync: UseMutateAsyncFunction<
|
||||||
|
AxiosResponse<SaveViewPayloadProps>,
|
||||||
|
Error,
|
||||||
|
SaveViewProps,
|
||||||
|
SaveViewPayloadProps
|
||||||
|
>;
|
||||||
|
handlePopOverClose: SaveViewWithNameProps['handlePopOverClose'];
|
||||||
|
redirectWithQueryBuilderData: QueryBuilderContextType['redirectWithQueryBuilderData'];
|
||||||
|
setNewViewName: Dispatch<SetStateAction<string>>;
|
||||||
|
}
|
69
frontend/src/container/ExplorerOptions/utils.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { Color } from '@signozhq/design-tokens';
|
||||||
|
import { showErrorNotification } from 'components/ExplorerCard/utils';
|
||||||
|
import { QueryParams } from 'constants/query';
|
||||||
|
import ROUTES from 'constants/routes';
|
||||||
|
import { mapQueryDataFromApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataFromApi';
|
||||||
|
import { DataSource } from 'types/common/queryBuilder';
|
||||||
|
|
||||||
|
import { SaveNewViewHandlerProps } from './types';
|
||||||
|
|
||||||
|
export const getRandomColor = (): Color => {
|
||||||
|
const colorKeys = Object.keys(Color) as (keyof typeof Color)[];
|
||||||
|
const randomKey = colorKeys[Math.floor(Math.random() * colorKeys.length)];
|
||||||
|
return Color[randomKey];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DATASOURCE_VS_ROUTES: Record<DataSource, string> = {
|
||||||
|
[DataSource.METRICS]: '',
|
||||||
|
[DataSource.TRACES]: ROUTES.TRACES_EXPLORER,
|
||||||
|
[DataSource.LOGS]: ROUTES.LOGS_EXPLORER,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const saveNewViewHandler = ({
|
||||||
|
saveViewAsync,
|
||||||
|
refetchAllView,
|
||||||
|
notifications,
|
||||||
|
handlePopOverClose,
|
||||||
|
viewName,
|
||||||
|
compositeQuery,
|
||||||
|
sourcePage,
|
||||||
|
extraData,
|
||||||
|
redirectWithQueryBuilderData,
|
||||||
|
panelType,
|
||||||
|
setNewViewName,
|
||||||
|
}: SaveNewViewHandlerProps): void => {
|
||||||
|
saveViewAsync(
|
||||||
|
{
|
||||||
|
viewName,
|
||||||
|
compositeQuery,
|
||||||
|
sourcePage,
|
||||||
|
extraData,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (data) => {
|
||||||
|
refetchAllView();
|
||||||
|
redirectWithQueryBuilderData(mapQueryDataFromApi(compositeQuery), {
|
||||||
|
[QueryParams.panelTypes]: panelType,
|
||||||
|
[QueryParams.viewName]: viewName,
|
||||||
|
[QueryParams.viewKey]: data.data.data,
|
||||||
|
});
|
||||||
|
notifications.success({
|
||||||
|
message: 'View Saved Successfully',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (err) => {
|
||||||
|
showErrorNotification(notifications, err);
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
handlePopOverClose();
|
||||||
|
setNewViewName('');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateRGBAFromHex = (hex: string, opacity: number): string =>
|
||||||
|
`rgba(${parseInt(hex.slice(1, 3), 16)}, ${parseInt(
|
||||||
|
hex.slice(3, 5),
|
||||||
|
16,
|
||||||
|
)}, ${parseInt(hex.slice(5, 7), 16)}, ${opacity})`;
|
@ -16,7 +16,10 @@ import {
|
|||||||
} from './styles';
|
} from './styles';
|
||||||
import { filterOptions, getSelectOptions } from './utils';
|
import { filterOptions, getSelectOptions } from './utils';
|
||||||
|
|
||||||
function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
|
function ExportPanelContainer({
|
||||||
|
isLoading,
|
||||||
|
onExport,
|
||||||
|
}: ExportPanelProps): JSX.Element {
|
||||||
const { t } = useTranslation(['dashboard']);
|
const { t } = useTranslation(['dashboard']);
|
||||||
|
|
||||||
const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>(
|
const [selectedDashboardId, setSelectedDashboardId] = useState<string | null>(
|
||||||
@ -118,4 +121,4 @@ function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ExportPanel;
|
export default ExportPanelContainer;
|
@ -1,13 +1,9 @@
|
|||||||
import { AlertOutlined, AreaChartOutlined } from '@ant-design/icons';
|
import { Modal } from 'antd';
|
||||||
import { Button, Modal, Space } from 'antd';
|
|
||||||
import { QueryParams } from 'constants/query';
|
|
||||||
import ROUTES from 'constants/routes';
|
|
||||||
import history from 'lib/history';
|
|
||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
import ExportPanelContainer from './ExportPanel';
|
import ExportPanelContainer from './ExportPanelContainer';
|
||||||
|
|
||||||
function ExportPanel({
|
function ExportPanel({
|
||||||
isLoading,
|
isLoading,
|
||||||
@ -20,53 +16,25 @@ function ExportPanel({
|
|||||||
setIsExport(value);
|
setIsExport(value);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onCreateAlertsHandler = useCallback(() => {
|
|
||||||
history.push(
|
|
||||||
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
|
||||||
JSON.stringify(query),
|
|
||||||
)}`,
|
|
||||||
);
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
const onCancel = (value: boolean) => (): void => {
|
const onCancel = (value: boolean) => (): void => {
|
||||||
onModalToggle(value);
|
onModalToggle(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAddToDashboard = (): void => {
|
|
||||||
setIsExport(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Modal
|
||||||
<Space size={24}>
|
footer={null}
|
||||||
<Button
|
onOk={onCancel(false)}
|
||||||
icon={<AreaChartOutlined />}
|
onCancel={onCancel(false)}
|
||||||
onClick={onAddToDashboard}
|
open={isExport}
|
||||||
type="primary"
|
centered
|
||||||
>
|
destroyOnClose
|
||||||
Add to Dashboard
|
>
|
||||||
</Button>
|
<ExportPanelContainer
|
||||||
|
query={query}
|
||||||
<Button onClick={onCreateAlertsHandler} icon={<AlertOutlined />}>
|
isLoading={isLoading}
|
||||||
Setup Alerts
|
onExport={onExport}
|
||||||
</Button>
|
/>
|
||||||
</Space>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
|
||||||
footer={null}
|
|
||||||
onOk={onCancel(false)}
|
|
||||||
onCancel={onCancel(false)}
|
|
||||||
open={isExport}
|
|
||||||
centered
|
|
||||||
destroyOnClose
|
|
||||||
>
|
|
||||||
<ExportPanelContainer
|
|
||||||
query={query}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onExport={onExport}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import GridPanelSwitch from 'container/GridPanelSwitch';
|
|||||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||||
|
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||||
import { useResizeObserver } from 'hooks/useDimensions';
|
import { useResizeObserver } from 'hooks/useDimensions';
|
||||||
@ -28,7 +29,7 @@ export interface ChartPreviewProps {
|
|||||||
query: Query | null;
|
query: Query | null;
|
||||||
graphType?: PANEL_TYPES;
|
graphType?: PANEL_TYPES;
|
||||||
selectedTime?: timePreferenceType;
|
selectedTime?: timePreferenceType;
|
||||||
selectedInterval?: Time;
|
selectedInterval?: Time | TimeV2;
|
||||||
headline?: JSX.Element;
|
headline?: JSX.Element;
|
||||||
alertDef?: AlertDef;
|
alertDef?: AlertDef;
|
||||||
userQueryKey?: string;
|
userQueryKey?: string;
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
.alert-tabs {
|
||||||
|
.ant-tabs-tab {
|
||||||
|
border: none !important;
|
||||||
|
margin-left: 0px !important;
|
||||||
|
padding: 0px !important;
|
||||||
|
|
||||||
|
.nav-btns {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.prom-ql-icon {
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-btn-default {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ant-tabs-tab-active {
|
||||||
|
.nav-btns {
|
||||||
|
background: var(--bg-slate-400) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-tabs-nav {
|
||||||
|
margin: 0px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.ant-tabs-nav::before {
|
||||||
|
border-bottom: none !important;
|
||||||
|
}
|
||||||
|
.ant-tabs-nav-list {
|
||||||
|
border: 1px solid var(--bg-slate-200);
|
||||||
|
}
|
||||||
|
.ant-tabs-tab + .ant-tabs-tab {
|
||||||
|
border-left: 1px solid var(--bg-slate-200) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-run-query {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightMode {
|
||||||
|
.alert-tabs {
|
||||||
|
.ant-tabs-nav-list {
|
||||||
|
border: 1px solid var(--bg-vanilla-300);
|
||||||
|
}
|
||||||
|
.ant-tabs-tab + .ant-tabs-tab {
|
||||||
|
border-left: 1px solid var(--bg-vanilla-200) !important;
|
||||||
|
}
|
||||||
|
.ant-tabs-tab-active {
|
||||||
|
.nav-btns {
|
||||||
|
background: var(--bg-vanilla-300) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
import { Button, Tabs } from 'antd';
|
import './QuerySection.styles.scss';
|
||||||
|
|
||||||
|
import { Button, Tabs, Tooltip } from 'antd';
|
||||||
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||||
import { QueryBuilder } from 'container/QueryBuilder';
|
import { QueryBuilder } from 'container/QueryBuilder';
|
||||||
import { useMemo } from 'react';
|
import { Atom, Play, Terminal } from 'lucide-react';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { AppState } from 'store/reducers';
|
import { AppState } from 'store/reducers';
|
||||||
@ -22,6 +25,7 @@ function QuerySection({
|
|||||||
}: QuerySectionProps): JSX.Element {
|
}: QuerySectionProps): JSX.Element {
|
||||||
// init namespace for translations
|
// init namespace for translations
|
||||||
const { t } = useTranslation('alerts');
|
const { t } = useTranslation('alerts');
|
||||||
|
const [currentTab, setCurrentTab] = useState(queryCategory);
|
||||||
|
|
||||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
@ -31,6 +35,7 @@ function QuerySection({
|
|||||||
featureResponse.refetch().then(() => {
|
featureResponse.refetch().then(() => {
|
||||||
setQueryCategory(queryType as EQueryType);
|
setQueryCategory(queryType as EQueryType);
|
||||||
});
|
});
|
||||||
|
setCurrentTab(queryType as EQueryType);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPromqlUI = (): JSX.Element => <PromqlSection />;
|
const renderPromqlUI = (): JSX.Element => <PromqlSection />;
|
||||||
@ -49,22 +54,61 @@ function QuerySection({
|
|||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
label: t('tab_qb'),
|
label: (
|
||||||
|
<Tooltip title="Query Builder">
|
||||||
|
<Button className="nav-btns">
|
||||||
|
<Atom size={14} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
key: EQueryType.QUERY_BUILDER,
|
key: EQueryType.QUERY_BUILDER,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('tab_chquery'),
|
label: (
|
||||||
|
<Tooltip title="ClickHouse">
|
||||||
|
<Button className="nav-btns">
|
||||||
|
<Terminal size={14} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
key: EQueryType.CLICKHOUSE,
|
key: EQueryType.CLICKHOUSE,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{ label: t('tab_qb'), key: EQueryType.QUERY_BUILDER },
|
{
|
||||||
{ label: t('tab_chquery'), key: EQueryType.CLICKHOUSE },
|
label: (
|
||||||
{ label: t('tab_promql'), key: EQueryType.PROM },
|
<Tooltip title="Query Builder">
|
||||||
|
<Button className="nav-btns">
|
||||||
|
<Atom size={14} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
key: EQueryType.QUERY_BUILDER,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<Tooltip title="ClickHouse">
|
||||||
|
<Button className="nav-btns">
|
||||||
|
<Terminal size={14} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
key: EQueryType.CLICKHOUSE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<Tooltip title="PromQL">
|
||||||
|
<Button className="nav-btns">
|
||||||
|
<img src="/Icons/promQL.svg" alt="Prom Ql" className="prom-ql-icon" />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
key: EQueryType.PROM,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
[t],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderTabs = (typ: AlertTypes): JSX.Element | null => {
|
const renderTabs = (typ: AlertTypes): JSX.Element | null => {
|
||||||
@ -73,40 +117,54 @@ function QuerySection({
|
|||||||
case AlertTypes.LOGS_BASED_ALERT:
|
case AlertTypes.LOGS_BASED_ALERT:
|
||||||
case AlertTypes.EXCEPTIONS_BASED_ALERT:
|
case AlertTypes.EXCEPTIONS_BASED_ALERT:
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<div className="alert-tabs">
|
||||||
type="card"
|
<Tabs
|
||||||
style={{ width: '100%' }}
|
type="card"
|
||||||
defaultActiveKey={EQueryType.QUERY_BUILDER}
|
style={{ width: '100%' }}
|
||||||
activeKey={queryCategory}
|
defaultActiveKey={currentTab}
|
||||||
onChange={handleQueryCategoryChange}
|
activeKey={currentTab}
|
||||||
tabBarExtraContent={
|
onChange={handleQueryCategoryChange}
|
||||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
tabBarExtraContent={
|
||||||
<Button type="primary" onClick={runQuery}>
|
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||||
Run Query
|
<Button
|
||||||
</Button>
|
type="primary"
|
||||||
</span>
|
onClick={runQuery}
|
||||||
}
|
className="stage-run-query"
|
||||||
items={tabs}
|
icon={<Play size={14} />}
|
||||||
/>
|
>
|
||||||
|
Stage & Run Query
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
items={tabs}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
case AlertTypes.METRICS_BASED_ALERT:
|
case AlertTypes.METRICS_BASED_ALERT:
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<div className="alert-tabs">
|
||||||
type="card"
|
<Tabs
|
||||||
style={{ width: '100%' }}
|
type="card"
|
||||||
defaultActiveKey={EQueryType.QUERY_BUILDER}
|
style={{ width: '100%' }}
|
||||||
activeKey={queryCategory}
|
defaultActiveKey={currentTab}
|
||||||
onChange={handleQueryCategoryChange}
|
activeKey={currentTab}
|
||||||
tabBarExtraContent={
|
onChange={handleQueryCategoryChange}
|
||||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
tabBarExtraContent={
|
||||||
<Button type="primary" onClick={runQuery}>
|
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||||
Run Query
|
<Button
|
||||||
</Button>
|
type="primary"
|
||||||
</span>
|
onClick={runQuery}
|
||||||
}
|
className="stage-run-query"
|
||||||
items={items}
|
icon={<Play size={14} />}
|
||||||
/>
|
>
|
||||||
|
Stage & Run Query
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
items={items}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -126,8 +184,8 @@ function QuerySection({
|
|||||||
<>
|
<>
|
||||||
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<div style={{ display: 'flex' }}>{renderTabs(alertType)}</div>
|
<div>{renderTabs(alertType)}</div>
|
||||||
{renderQuerySection(queryCategory)}
|
{renderQuerySection(currentTab)}
|
||||||
</FormContainer>
|
</FormContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -83,7 +83,7 @@ function RuleOptions({
|
|||||||
</InlineSelect>
|
</InlineSelect>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderThresholdMatchOpts = (): JSX.Element => (
|
const renderMatchOpts = (): JSX.Element => (
|
||||||
<InlineSelect
|
<InlineSelect
|
||||||
getPopupContainer={popupContainer}
|
getPopupContainer={popupContainer}
|
||||||
defaultValue={defaultMatchType}
|
defaultValue={defaultMatchType}
|
||||||
@ -98,17 +98,13 @@ function RuleOptions({
|
|||||||
</InlineSelect>
|
</InlineSelect>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderPromMatchOpts = (): JSX.Element => (
|
const onChangeEvalWindow = (value: string | unknown): void => {
|
||||||
<InlineSelect
|
const ew = (value as string) || alertDef.evalWindow;
|
||||||
getPopupContainer={popupContainer}
|
setAlertDef({
|
||||||
defaultValue={defaultMatchType}
|
...alertDef,
|
||||||
style={{ minWidth: '130px' }}
|
evalWindow: ew,
|
||||||
value={alertDef.condition?.matchType}
|
});
|
||||||
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
};
|
||||||
>
|
|
||||||
<Select.Option value="1">{t('option_atleastonce')}</Select.Option>
|
|
||||||
</InlineSelect>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderEvalWindows = (): JSX.Element => (
|
const renderEvalWindows = (): JSX.Element => (
|
||||||
<InlineSelect
|
<InlineSelect
|
||||||
@ -116,13 +112,7 @@ function RuleOptions({
|
|||||||
defaultValue={defaultEvalWindow}
|
defaultValue={defaultEvalWindow}
|
||||||
style={{ minWidth: '120px' }}
|
style={{ minWidth: '120px' }}
|
||||||
value={alertDef.evalWindow}
|
value={alertDef.evalWindow}
|
||||||
onChange={(value: string | unknown): void => {
|
onChange={onChangeEvalWindow}
|
||||||
const ew = (value as string) || alertDef.evalWindow;
|
|
||||||
setAlertDef({
|
|
||||||
...alertDef,
|
|
||||||
evalWindow: ew,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Select.Option value="5m0s">{t('option_5min')}</Select.Option>
|
<Select.Option value="5m0s">{t('option_5min')}</Select.Option>
|
||||||
<Select.Option value="10m0s">{t('option_10min')}</Select.Option>
|
<Select.Option value="10m0s">{t('option_10min')}</Select.Option>
|
||||||
@ -133,6 +123,20 @@ function RuleOptions({
|
|||||||
</InlineSelect>
|
</InlineSelect>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderPromEvalWindows = (): JSX.Element => (
|
||||||
|
<InlineSelect
|
||||||
|
getPopupContainer={popupContainer}
|
||||||
|
defaultValue={defaultEvalWindow}
|
||||||
|
style={{ minWidth: '120px' }}
|
||||||
|
value={alertDef.evalWindow}
|
||||||
|
onChange={onChangeEvalWindow}
|
||||||
|
>
|
||||||
|
<Select.Option value="5m0s">{t('option_5min')}</Select.Option>
|
||||||
|
<Select.Option value="10m0s">{t('option_10min')}</Select.Option>
|
||||||
|
<Select.Option value="15m0s">{t('option_15min')}</Select.Option>
|
||||||
|
</InlineSelect>
|
||||||
|
);
|
||||||
|
|
||||||
const renderThresholdRuleOpts = (): JSX.Element => (
|
const renderThresholdRuleOpts = (): JSX.Element => (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Typography.Text>
|
<Typography.Text>
|
||||||
@ -147,7 +151,7 @@ function RuleOptions({
|
|||||||
onChange={onChangeSelectedQueryName}
|
onChange={onChangeSelectedQueryName}
|
||||||
/>
|
/>
|
||||||
<Typography.Text>is</Typography.Text>
|
<Typography.Text>is</Typography.Text>
|
||||||
{renderCompareOps()} {t('text_condition2')} {renderThresholdMatchOpts()}{' '}
|
{renderCompareOps()} {t('text_condition2')} {renderMatchOpts()}{' '}
|
||||||
{t('text_condition3')} {renderEvalWindows()}
|
{t('text_condition3')} {renderEvalWindows()}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@ -167,7 +171,8 @@ function RuleOptions({
|
|||||||
onChange={onChangeSelectedQueryName}
|
onChange={onChangeSelectedQueryName}
|
||||||
/>
|
/>
|
||||||
<Typography.Text>is</Typography.Text>
|
<Typography.Text>is</Typography.Text>
|
||||||
{renderCompareOps()} {t('text_condition2')} {renderPromMatchOpts()}
|
{renderCompareOps()} {t('text_condition2')} {renderMatchOpts()}
|
||||||
|
{t('text_condition3')} {renderPromEvalWindows()}
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
);
|
);
|
||||||
|
@ -142,6 +142,10 @@ function FormAlertRules({
|
|||||||
// onQueryCategoryChange handles changes to query category
|
// onQueryCategoryChange handles changes to query category
|
||||||
// in state as well as sets additional defaults
|
// in state as well as sets additional defaults
|
||||||
const onQueryCategoryChange = (val: EQueryType): void => {
|
const onQueryCategoryChange = (val: EQueryType): void => {
|
||||||
|
const element = document.getElementById('top');
|
||||||
|
if (element) {
|
||||||
|
element.scrollIntoView({ behavior: 'smooth' });
|
||||||
|
}
|
||||||
if (val === EQueryType.PROM) {
|
if (val === EQueryType.PROM) {
|
||||||
setAlertDef({
|
setAlertDef({
|
||||||
...alertDef,
|
...alertDef,
|
||||||
@ -465,7 +469,7 @@ function FormAlertRules({
|
|||||||
<>
|
<>
|
||||||
{Element}
|
{Element}
|
||||||
|
|
||||||
<PanelContainer>
|
<PanelContainer id="top">
|
||||||
<StyledLeftContainer flex="5 1 600px" md={18}>
|
<StyledLeftContainer flex="5 1 600px" md={18}>
|
||||||
<MainFormContainer
|
<MainFormContainer
|
||||||
initialValues={initialValue}
|
initialValues={initialValue}
|
||||||
|
@ -76,6 +76,10 @@ export const FormContainer = styled(Card)`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TextareaMedium = styled(TextArea)`
|
export const TextareaMedium = styled(TextArea)`
|
||||||
|
@ -3,6 +3,7 @@ import { LoadingOutlined } from '@ant-design/icons';
|
|||||||
import { Button, Card, Col, Divider, Modal, Row, Spin, Typography } from 'antd';
|
import { Button, Card, Col, Divider, Modal, Row, Spin, Typography } from 'antd';
|
||||||
import setRetentionApi from 'api/settings/setRetention';
|
import setRetentionApi from 'api/settings/setRetention';
|
||||||
import TextToolTip from 'components/TextToolTip';
|
import TextToolTip from 'components/TextToolTip';
|
||||||
|
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
||||||
import useComponentPermission from 'hooks/useComponentPermission';
|
import useComponentPermission from 'hooks/useComponentPermission';
|
||||||
import { useNotifications } from 'hooks/useNotifications';
|
import { useNotifications } from 'hooks/useNotifications';
|
||||||
import find from 'lodash-es/find';
|
import find from 'lodash-es/find';
|
||||||
@ -24,6 +25,7 @@ import {
|
|||||||
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
|
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
|
||||||
} from 'types/api/settings/getRetention';
|
} from 'types/api/settings/getRetention';
|
||||||
import AppReducer from 'types/reducer/app';
|
import AppReducer from 'types/reducer/app';
|
||||||
|
import { isCloudUser } from 'utils/app';
|
||||||
|
|
||||||
import Retention from './Retention';
|
import Retention from './Retention';
|
||||||
import StatusMessage from './StatusMessage';
|
import StatusMessage from './StatusMessage';
|
||||||
@ -394,6 +396,8 @@ function GeneralSettings({
|
|||||||
onModalToggleHandler(type);
|
onModalToggleHandler(type);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isCloudUserVal = isCloudUser();
|
||||||
|
|
||||||
const renderConfig = [
|
const renderConfig = [
|
||||||
{
|
{
|
||||||
name: 'Metrics',
|
name: 'Metrics',
|
||||||
@ -521,7 +525,7 @@ function GeneralSettings({
|
|||||||
return (
|
return (
|
||||||
<Fragment key={category.name}>
|
<Fragment key={category.name}>
|
||||||
<Col xs={22} xl={11} key={category.name} style={{ margin: '0.5rem' }}>
|
<Col xs={22} xl={11} key={category.name} style={{ margin: '0.5rem' }}>
|
||||||
<Card style={{ height: '100%', minHeight: 300 }}>
|
<Card style={{ height: '100%' }}>
|
||||||
<Typography.Title style={{ margin: 0 }} level={3}>
|
<Typography.Title style={{ margin: 0 }} level={3}>
|
||||||
{category.name}
|
{category.name}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
@ -542,38 +546,43 @@ function GeneralSettings({
|
|||||||
hide={!!retentionField.hide}
|
hide={!!retentionField.hide}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<ActionItemsContainer>
|
|
||||||
<Button
|
{!isCloudUserVal && (
|
||||||
type="primary"
|
<>
|
||||||
onClick={category.save.modalOpen}
|
<ActionItemsContainer>
|
||||||
disabled={category.save.isDisabled}
|
<Button
|
||||||
>
|
type="primary"
|
||||||
{category.save.saveButtonText}
|
onClick={category.save.modalOpen}
|
||||||
</Button>
|
disabled={category.save.isDisabled}
|
||||||
{category.statusComponent}
|
>
|
||||||
</ActionItemsContainer>
|
{category.save.saveButtonText}
|
||||||
<Modal
|
</Button>
|
||||||
title={t('retention_confirmation')}
|
{category.statusComponent}
|
||||||
focusTriggerAfterClose
|
</ActionItemsContainer>
|
||||||
forceRender
|
<Modal
|
||||||
destroyOnClose
|
title={t('retention_confirmation')}
|
||||||
closable
|
focusTriggerAfterClose
|
||||||
onCancel={(): void =>
|
forceRender
|
||||||
onModalToggleHandler(category.name.toLowerCase() as TTTLType)
|
destroyOnClose
|
||||||
}
|
closable
|
||||||
onOk={(): Promise<void> =>
|
onCancel={(): void =>
|
||||||
onOkHandler(category.name.toLowerCase() as TTTLType)
|
onModalToggleHandler(category.name.toLowerCase() as TTTLType)
|
||||||
}
|
}
|
||||||
centered
|
onOk={(): Promise<void> =>
|
||||||
open={category.save.modal}
|
onOkHandler(category.name.toLowerCase() as TTTLType)
|
||||||
confirmLoading={category.save.apiLoading}
|
}
|
||||||
>
|
centered
|
||||||
<Typography>
|
open={category.save.modal}
|
||||||
{t('retention_confirmation_description', {
|
confirmLoading={category.save.apiLoading}
|
||||||
name: category.name.toLowerCase(),
|
>
|
||||||
})}
|
<Typography>
|
||||||
</Typography>
|
{t('retention_confirmation_description', {
|
||||||
</Modal>
|
name: category.name.toLowerCase(),
|
||||||
|
})}
|
||||||
|
</Typography>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
@ -587,16 +596,20 @@ function GeneralSettings({
|
|||||||
{Element}
|
{Element}
|
||||||
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
|
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
|
||||||
<ErrorTextContainer>
|
<ErrorTextContainer>
|
||||||
<TextToolTip
|
{!isCloudUserVal && (
|
||||||
{...{
|
<TextToolTip
|
||||||
text: `More details on how to set retention period`,
|
{...{
|
||||||
url: 'https://signoz.io/docs/userguide/retention-period/',
|
text: `More details on how to set retention period`,
|
||||||
}}
|
url: 'https://signoz.io/docs/userguide/retention-period/',
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{errorText && <ErrorText>{errorText}</ErrorText>}
|
{errorText && <ErrorText>{errorText}</ErrorText>}
|
||||||
</ErrorTextContainer>
|
</ErrorTextContainer>
|
||||||
|
|
||||||
<Row justify="start">{renderConfig}</Row>
|
<Row justify="start">{renderConfig}</Row>
|
||||||
|
|
||||||
|
{isCloudUserVal && <GeneralSettingsCloud />}
|
||||||
</Col>
|
</Col>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import { isCloudUser } from 'utils/app';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
@ -85,9 +86,13 @@ function Retention({
|
|||||||
func(null);
|
func(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hide) {
|
if (hide) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isCloudUserVal = isCloudUser();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RetentionContainer>
|
<RetentionContainer>
|
||||||
<Row justify="space-between">
|
<Row justify="space-between">
|
||||||
@ -98,12 +103,14 @@ function Retention({
|
|||||||
<RetentionFieldInputContainer>
|
<RetentionFieldInputContainer>
|
||||||
<Input
|
<Input
|
||||||
value={selectedValue && selectedValue >= 0 ? selectedValue : ''}
|
value={selectedValue && selectedValue >= 0 ? selectedValue : ''}
|
||||||
|
disabled={isCloudUserVal}
|
||||||
onChange={(e): void => onChangeHandler(e, setSelectedValue)}
|
onChange={(e): void => onChangeHandler(e, setSelectedValue)}
|
||||||
style={{ width: 75 }}
|
style={{ width: 75 }}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={selectedTimeUnit}
|
value={selectedTimeUnit}
|
||||||
onChange={currentSelectedOption}
|
onChange={currentSelectedOption}
|
||||||
|
disabled={isCloudUserVal}
|
||||||
style={{ width: 100 }}
|
style={{ width: 100 }}
|
||||||
>
|
>
|
||||||
{menuItems}
|
{menuItems}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { green, orange, volcano } from '@ant-design/colors';
|
import { green, orange, volcano } from '@ant-design/colors';
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import { Card, Col, Row } from 'antd';
|
import { Card, Col } from 'antd';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { TStatus } from 'types/api/settings/getRetention';
|
import { TStatus } from 'types/api/settings/getRetention';
|
||||||
@ -43,9 +43,16 @@ function StatusMessage({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Row style={{ gap: '1rem', alignItems: 'center', justifyContent: 'center' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
gap: '1rem',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Col xs={1}>
|
<Col xs={1}>
|
||||||
<InfoCircleOutlined style={{ fontSize: '1.5rem' }} />
|
<InfoCircleOutlined style={{ fontSize: '1rem' }} />
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col
|
<Col
|
||||||
@ -56,7 +63,7 @@ function StatusMessage({
|
|||||||
>
|
>
|
||||||
{statusMessage}
|
{statusMessage}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
.general-settings-container {
|
.general-settings-container {
|
||||||
|
margin: 16px 8px;
|
||||||
|
|
||||||
.ant-card-body {
|
.ant-card-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
padding: 8px;
|
||||||
|
margin: 16px 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ function HeaderContainer(): JSX.Element {
|
|||||||
const isDarkMode = useIsDarkMode();
|
const isDarkMode = useIsDarkMode();
|
||||||
const { toggleTheme } = useThemeMode();
|
const { toggleTheme } = useThemeMode();
|
||||||
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
|
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
|
||||||
const [homeRoute, setHomeRoute] = useState(ROUTES.APPLICATION);
|
const [homeRoute, setHomeRoute] = useState<string>(ROUTES.APPLICATION);
|
||||||
|
|
||||||
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>(false);
|
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
@ -84,6 +84,7 @@ export default function IngestionSettings(): JSX.Element {
|
|||||||
pagination={false}
|
pagination={false}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
|
bordered
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Card, Typography } from 'antd';
|
import { Card, Typography } from 'antd';
|
||||||
import LogDetail from 'components/LogDetail';
|
import LogDetail from 'components/LogDetail';
|
||||||
|
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||||
import ListLogView from 'components/Logs/ListLogView';
|
import ListLogView from 'components/Logs/ListLogView';
|
||||||
import RawLogView from 'components/Logs/RawLogView';
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
import Spinner from 'components/Spinner';
|
import Spinner from 'components/Spinner';
|
||||||
@ -13,7 +14,6 @@ import { Heading } from 'container/LogsTable/styles';
|
|||||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||||
import useFontFaceObserver from 'hooks/useFontObserver';
|
|
||||||
import { useEventSource } from 'providers/EventSource';
|
import { useEventSource } from 'providers/EventSource';
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -51,19 +51,6 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
|||||||
[logs, activeLogId],
|
[logs, activeLogId],
|
||||||
);
|
);
|
||||||
|
|
||||||
useFontFaceObserver(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
family: 'Fira Code',
|
|
||||||
weight: '300',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
options.format === 'raw',
|
|
||||||
{
|
|
||||||
timeout: 5000,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectedFields = convertKeysToColumnFields(options.selectColumns);
|
const selectedFields = convertKeysToColumnFields(options.selectColumns);
|
||||||
|
|
||||||
const getItemContent = useCallback(
|
const getItemContent = useCallback(
|
||||||
@ -150,6 +137,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
|||||||
</InfinityWrapperStyled>
|
</InfinityWrapperStyled>
|
||||||
)}
|
)}
|
||||||
<LogDetail
|
<LogDetail
|
||||||
|
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||||
log={activeLog}
|
log={activeLog}
|
||||||
onClose={onClearActiveLog}
|
onClose={onClearActiveLog}
|
||||||
onAddToQuery={onAddToQuery}
|
onAddToQuery={onAddToQuery}
|
||||||
|
@ -29,11 +29,11 @@ function ActionItem({
|
|||||||
() => (
|
() => (
|
||||||
<Col>
|
<Col>
|
||||||
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.IN)}>
|
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.IN)}>
|
||||||
<PlusCircleOutlined /> Filter for value
|
<PlusCircleOutlined size={12} /> Filter for value
|
||||||
</Button>
|
</Button>
|
||||||
<br />
|
<br />
|
||||||
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.NIN)}>
|
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.NIN)}>
|
||||||
<MinusCircleOutlined /> Filter out value
|
<MinusCircleOutlined size={12} /> Filter out value
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
),
|
),
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
.log-context-container {
|
||||||
|
border: 1px solid var(--bg-slate-400);
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
import './ContextView.styles.scss';
|
||||||
|
|
||||||
|
import RawLogView from 'components/Logs/RawLogView';
|
||||||
|
import LogsContextList from 'container/LogsContextList';
|
||||||
|
import { ORDERBY_FILTERS } from 'container/QueryBuilder/filters/OrderByFilter/config';
|
||||||
|
import { ILog } from 'types/api/logs/log';
|
||||||
|
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||||
|
|
||||||
|
interface LogContextProps {
|
||||||
|
log: ILog;
|
||||||
|
contextQuery: Query | undefined;
|
||||||
|
filters: TagFilter | null;
|
||||||
|
isEdit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContextView({
|
||||||
|
log,
|
||||||
|
filters,
|
||||||
|
contextQuery,
|
||||||
|
isEdit,
|
||||||
|
}: LogContextProps): JSX.Element {
|
||||||
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
|
if (!contextQuery) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="log-context-container">
|
||||||
|
<LogsContextList
|
||||||
|
className="logs-context-list-asc"
|
||||||
|
order={ORDERBY_FILTERS.ASC}
|
||||||
|
filters={filters}
|
||||||
|
isEdit={isEdit}
|
||||||
|
log={log}
|
||||||
|
query={contextQuery}
|
||||||
|
/>
|
||||||
|
<RawLogView
|
||||||
|
isActiveLog
|
||||||
|
isReadOnly
|
||||||
|
isTextOverflowEllipsisDisabled={false}
|
||||||
|
data={log}
|
||||||
|
linesPerRow={1}
|
||||||
|
/>
|
||||||
|
<LogsContextList
|
||||||
|
className="logs-context-list-desc"
|
||||||
|
order={ORDERBY_FILTERS.DESC}
|
||||||
|
filters={filters}
|
||||||
|
isEdit={isEdit}
|
||||||
|
log={log}
|
||||||
|
query={contextQuery}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContextView;
|