2
.github/workflows/dependency-review.yml
vendored
@ -19,4 +19,4 @@ jobs:
|
||||
- name: 'Dependency Review'
|
||||
with:
|
||||
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
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup golang
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: "1.21"
|
||||
|
||||
- name: Build query-service image
|
||||
env:
|
||||
DEV_BUILD: 1
|
||||
@ -65,9 +70,9 @@ jobs:
|
||||
- name: Kick off a sample-app workload
|
||||
run: |
|
||||
# start the locust swarm
|
||||
kubectl -n sample-application run strzal --image=djbingham/curl \
|
||||
--restart='OnFailure' -i --rm --command -- curl -X POST -F \
|
||||
'locust_count=6' -F 'hatch_rate=2' http://locust-master:8089/swarm
|
||||
kubectl --namespace sample-application run strzal --image=djbingham/curl \
|
||||
--restart='OnFailure' -i --tty --rm --command -- curl -X POST -F \
|
||||
'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
|
||||
id: get-subdomain
|
||||
|
20
.github/workflows/push.yaml
vendored
@ -20,13 +20,13 @@ jobs:
|
||||
with:
|
||||
go-version: "1.21"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@ -64,13 +64,13 @@ jobs:
|
||||
with:
|
||||
go-version: "1.21"
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
uses: docker/setup-qemu-action@v3
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@ -115,11 +115,11 @@ jobs:
|
||||
run: npm run lint
|
||||
continue-on-error: true
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@ -164,11 +164,11 @@ jobs:
|
||||
run: npm run lint
|
||||
continue-on-error: true
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
version: latest
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
2
.github/workflows/staging-deployment.yaml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
environment: staging
|
||||
steps:
|
||||
- name: Executing remote ssh commands using ssh key
|
||||
uses: appleboy/ssh-action@v0.1.8
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
env:
|
||||
GITHUB_BRANCH: develop
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
|
7
.github/workflows/testing-deployment.yaml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
if: ${{ github.event.label.name == 'testing-deploy' }}
|
||||
steps:
|
||||
- name: Executing remote ssh commands using ssh key
|
||||
uses: appleboy/ssh-action@v0.1.8
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
env:
|
||||
GITHUB_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
@ -33,8 +33,11 @@ jobs:
|
||||
git add .
|
||||
git stash push -m "stashed on $(date --iso-8601=seconds)"
|
||||
git fetch origin
|
||||
git checkout ${GITHUB_BRANCH}
|
||||
git checkout develop
|
||||
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-frontend-amd64
|
||||
make run-signoz
|
@ -108,7 +108,7 @@ We support [OpenTelemetry](https://opentelemetry.io) as the library which you ca
|
||||
|
||||
- Java
|
||||
- Python
|
||||
- NodeJS
|
||||
- Node.js
|
||||
- Go
|
||||
- PHP
|
||||
- .NET
|
||||
|
@ -1,7 +1,7 @@
|
||||
version: "3.9"
|
||||
|
||||
x-clickhouse-defaults: &clickhouse-defaults
|
||||
image: clickhouse/clickhouse-server:23.11.1-alpine
|
||||
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||
tty: true
|
||||
deploy:
|
||||
restart_policy:
|
||||
@ -146,7 +146,7 @@ services:
|
||||
condition: on-failure
|
||||
|
||||
query-service:
|
||||
image: signoz/query-service:0.38.2
|
||||
image: signoz/query-service:0.39.0
|
||||
command:
|
||||
[
|
||||
"-config=/root/config/prometheus.yml",
|
||||
@ -186,7 +186,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:0.38.2
|
||||
image: signoz/frontend:0.39.0
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
@ -199,7 +199,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
otel-collector:
|
||||
image: signoz/signoz-otel-collector:0.88.11
|
||||
image: signoz/signoz-otel-collector:0.88.12
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
@ -237,7 +237,7 @@ services:
|
||||
- query-service
|
||||
|
||||
otel-collector-migrator:
|
||||
image: signoz/signoz-schema-migrator:0.88.11
|
||||
image: signoz/signoz-schema-migrator:0.88.12
|
||||
deploy:
|
||||
restart_policy:
|
||||
condition: on-failure
|
||||
|
@ -123,15 +123,7 @@ exporters:
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
timeout: 5s
|
||||
sending_queue:
|
||||
queue_size: 100
|
||||
retry_on_failure:
|
||||
enabled: true
|
||||
initial_interval: 5s
|
||||
max_interval: 30s
|
||||
max_elapsed_time: 300s
|
||||
|
||||
timeout: 10s
|
||||
extensions:
|
||||
health_check:
|
||||
endpoint: 0.0.0.0:13133
|
||||
|
@ -19,7 +19,7 @@ services:
|
||||
- ZOO_AUTOPURGE_INTERVAL=1
|
||||
|
||||
clickhouse:
|
||||
image: clickhouse/clickhouse-server:23.7.3-alpine
|
||||
image: clickhouse/clickhouse-server:24.1.2-alpine
|
||||
container_name: signoz-clickhouse
|
||||
# ports:
|
||||
# - "9000:9000"
|
||||
@ -66,7 +66,7 @@ services:
|
||||
- --storage.path=/data
|
||||
|
||||
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
|
||||
command:
|
||||
- "--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`
|
||||
otel-collector:
|
||||
container_name: signoz-otel-collector
|
||||
image: signoz/signoz-otel-collector:0.88.11
|
||||
image: signoz/signoz-otel-collector:0.88.12
|
||||
command:
|
||||
[
|
||||
"--config=/etc/otel-collector-config.yaml",
|
||||
|
@ -3,7 +3,7 @@ version: "2.4"
|
||||
x-clickhouse-defaults: &clickhouse-defaults
|
||||
restart: on-failure
|
||||
# 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
|
||||
depends_on:
|
||||
- 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`
|
||||
|
||||
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
|
||||
command:
|
||||
[
|
||||
@ -203,7 +203,7 @@ services:
|
||||
<<: *db-depend
|
||||
|
||||
frontend:
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.38.2}
|
||||
image: signoz/frontend:${DOCKER_TAG:-0.39.0}
|
||||
container_name: signoz-frontend
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
@ -215,7 +215,7 @@ services:
|
||||
- ../common/nginx-config.conf:/etc/nginx/conf.d/default.conf
|
||||
|
||||
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
|
||||
command:
|
||||
- "--dsn=tcp://clickhouse:9000"
|
||||
@ -229,7 +229,7 @@ services:
|
||||
|
||||
|
||||
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
|
||||
command:
|
||||
[
|
||||
|
@ -135,14 +135,7 @@ exporters:
|
||||
clickhouselogsexporter:
|
||||
dsn: tcp://clickhouse:9000/
|
||||
docker_multi_node_cluster: ${DOCKER_MULTI_NODE_CLUSTER}
|
||||
timeout: 5s
|
||||
sending_queue:
|
||||
queue_size: 100
|
||||
retry_on_failure:
|
||||
enabled: true
|
||||
initial_interval: 5s
|
||||
max_interval: 30s
|
||||
max_elapsed_time: 300s
|
||||
timeout: 10s
|
||||
|
||||
service:
|
||||
telemetry:
|
||||
|
@ -1,5 +1,5 @@
|
||||
# use a minimal alpine image
|
||||
FROM alpine:3.18.5
|
||||
FROM alpine:3.18.6
|
||||
|
||||
# Add Maintainer Info
|
||||
LABEL maintainer="signoz"
|
||||
|
@ -139,17 +139,13 @@ func (lm *Manager) UploadUsage() {
|
||||
|
||||
zap.S().Info("uploading usage data")
|
||||
|
||||
// Try to get the org name
|
||||
orgName := ""
|
||||
orgNames, err := lm.modelDao.GetOrgs(ctx)
|
||||
if err != nil {
|
||||
zap.S().Errorf("failed to get org data: %v", zap.Error(err))
|
||||
} else {
|
||||
if len(orgNames) != 1 {
|
||||
zap.S().Errorf("expected one org but got %d orgs", len(orgNames))
|
||||
} else {
|
||||
orgName = orgNames[0].Name
|
||||
}
|
||||
orgNames, orgError := lm.modelDao.GetOrgs(ctx)
|
||||
if orgError != nil {
|
||||
zap.S().Errorf("failed to get org data: %v", zap.Error(orgError))
|
||||
}
|
||||
if len(orgNames) == 1 {
|
||||
orgName = orgNames[0].Name
|
||||
}
|
||||
|
||||
usagesPayload := []model.Usage{}
|
||||
|
@ -22,7 +22,7 @@ const config: Config.InitialOptions = {
|
||||
'^.+\\.(js|jsx)$': 'babel-jest',
|
||||
},
|
||||
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'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/public/'],
|
||||
|
@ -36,7 +36,9 @@
|
||||
"@mdx-js/loader": "2.3.0",
|
||||
"@mdx-js/react": "2.3.0",
|
||||
"@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",
|
||||
"@xstate/react": "^3.0.0",
|
||||
"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",
|
||||
"WORKSPACE_LOCKED": "SigNoz | Workspace Locked",
|
||||
"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
|
||||
str="{
|
||||
\"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
|
||||
|
||||
@ -22,4 +22,4 @@ code=$?
|
||||
# delete temp config
|
||||
rm ./tsconfig.tmp
|
||||
|
||||
exit $code
|
||||
exit $code
|
||||
|
@ -160,7 +160,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
if (currentRoute) {
|
||||
const { isPrivate, key } = currentRoute;
|
||||
|
||||
if (isPrivate && key !== ROUTES.WORKSPACE_LOCKED) {
|
||||
if (isPrivate && key !== String(ROUTES.WORKSPACE_LOCKED)) {
|
||||
handlePrivateRoutes(key);
|
||||
} else {
|
||||
// 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 AppLayout from 'container/AppLayout';
|
||||
import useAnalytics from 'hooks/analytics/useAnalytics';
|
||||
import { KeyboardHotkeysProvider } from 'hooks/hotkeys/useKeyboardHotkeys';
|
||||
import { useThemeConfig } from 'hooks/useDarkMode';
|
||||
import useGetFeatureFlag from 'hooks/useGetFeatureFlag';
|
||||
import useLicense, { LICENSE_PLAN_KEY } from 'hooks/useLicense';
|
||||
@ -177,22 +178,24 @@ function App(): JSX.Element {
|
||||
<ResourceProvider>
|
||||
<QueryBuilderProvider>
|
||||
<DashboardProvider>
|
||||
<AppLayout>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }) => (
|
||||
<Route
|
||||
key={`${path}`}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
))}
|
||||
<KeyboardHotkeysProvider>
|
||||
<AppLayout>
|
||||
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
|
||||
<Switch>
|
||||
{routes.map(({ path, component, exact }) => (
|
||||
<Route
|
||||
key={`${path}`}
|
||||
exact={exact}
|
||||
path={path}
|
||||
component={component}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
<Route path="*" component={NotFound} />
|
||||
</Switch>
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
</KeyboardHotkeysProvider>
|
||||
</DashboardProvider>
|
||||
</QueryBuilderProvider>
|
||||
</ResourceProvider>
|
||||
|
@ -15,9 +15,20 @@ export const ServiceMapPage = Loadable(
|
||||
() => 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(
|
||||
() =>
|
||||
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(
|
||||
@ -171,3 +182,7 @@ export const WorkspaceBlocked = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "WorkspaceLocked" */ 'pages/WorkspaceLocked'),
|
||||
);
|
||||
|
||||
export const ShortcutsPage = Loadable(
|
||||
() => import(/* webpackChunkName: "ShortcutsPage" */ 'pages/Shortcuts'),
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import Shortcuts from 'pages/Shortcuts/Shortcuts';
|
||||
import WorkspaceBlocked from 'pages/WorkspaceLocked';
|
||||
import { RouteProps } from 'react-router-dom';
|
||||
|
||||
@ -21,6 +22,7 @@ import {
|
||||
Logs,
|
||||
LogsExplorer,
|
||||
LogsIndexToFields,
|
||||
LogsSaveViews,
|
||||
MySettings,
|
||||
NewDashboardPage,
|
||||
OldLogsExplorer,
|
||||
@ -39,6 +41,7 @@ import {
|
||||
TraceDetail,
|
||||
TraceFilter,
|
||||
TracesExplorer,
|
||||
TracesSaveViews,
|
||||
UnAuthorized,
|
||||
UsageExplorerPage,
|
||||
} from './pageComponents';
|
||||
@ -86,6 +89,13 @@ const routes: AppRoutes[] = [
|
||||
exact: true,
|
||||
key: 'SERVICE_MAP',
|
||||
},
|
||||
{
|
||||
path: ROUTES.LOGS_SAVE_VIEWS,
|
||||
component: LogsSaveViews,
|
||||
isPrivate: true,
|
||||
exact: true,
|
||||
key: 'LOGS_SAVE_VIEWS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACE_DETAIL,
|
||||
exact: true,
|
||||
@ -163,6 +173,13 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'TRACES_EXPLORER',
|
||||
},
|
||||
{
|
||||
path: ROUTES.TRACES_SAVE_VIEWS,
|
||||
exact: true,
|
||||
component: TracesSaveViews,
|
||||
isPrivate: true,
|
||||
key: 'TRACES_SAVE_VIEWS',
|
||||
},
|
||||
{
|
||||
path: ROUTES.CHANNELS_NEW,
|
||||
exact: true,
|
||||
@ -303,6 +320,13 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
key: 'WORKSPACE_LOCKED',
|
||||
},
|
||||
{
|
||||
path: ROUTES.SHORTCUTS,
|
||||
exact: true,
|
||||
component: Shortcuts,
|
||||
isPrivate: true,
|
||||
key: 'SHORTCUTS',
|
||||
},
|
||||
];
|
||||
|
||||
export const SUPPORT_ROUTE: AppRoutes = {
|
||||
|
@ -33,6 +33,7 @@
|
||||
.timeSelection-input {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
height: 33px;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
padding-left: 0px !important;
|
||||
@ -59,6 +60,26 @@
|
||||
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 {
|
||||
.time-options-container {
|
||||
.time-options-item {
|
||||
@ -93,4 +114,8 @@
|
||||
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 cx from 'classnames';
|
||||
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
|
||||
import { Options } from 'container/TopNav/DateTimeSelection/config';
|
||||
import {
|
||||
FixedDurationSuggestionOptions,
|
||||
RelativeDurationSuggestionOptions,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import dayjs from 'dayjs';
|
||||
import { defaultTo, noop } from 'lodash-es';
|
||||
import debounce from 'lodash-es/debounce';
|
||||
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 CustomTimePickerPopoverContent from './CustomTimePickerPopoverContent';
|
||||
|
||||
const maxAllowedMinTimeInMonths = 6;
|
||||
|
||||
interface CustomTimePickerProps {
|
||||
onSelect: (value: string) => void;
|
||||
onError: (value: boolean) => void;
|
||||
items: any[];
|
||||
selectedValue: string;
|
||||
selectedTime: string;
|
||||
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({
|
||||
@ -28,9 +50,15 @@ function CustomTimePicker({
|
||||
items,
|
||||
selectedValue,
|
||||
selectedTime,
|
||||
open,
|
||||
setOpen,
|
||||
onValidCustomDateChange,
|
||||
newPopover,
|
||||
customDateTimeVisible,
|
||||
setCustomDTPickerVisible,
|
||||
onCustomDateHandler,
|
||||
handleGoLive,
|
||||
}: CustomTimePickerProps): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [
|
||||
selectedTimePlaceholderValue,
|
||||
setSelectedTimePlaceholderValue,
|
||||
@ -41,6 +69,7 @@ function CustomTimePicker({
|
||||
const [inputErrorMessage, setInputErrorMessage] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const location = useLocation();
|
||||
const [isInputFocused, setIsInputFocused] = useState(false);
|
||||
|
||||
const getSelectedTimeRangeLabel = (
|
||||
@ -56,6 +85,20 @@ function CustomTimePicker({
|
||||
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 '';
|
||||
};
|
||||
@ -111,7 +154,7 @@ function CustomTimePicker({
|
||||
break;
|
||||
}
|
||||
|
||||
if (minTime && minTime < maxAllowedMinTime) {
|
||||
if (minTime && (!minTime.isValid() || minTime < maxAllowedMinTime)) {
|
||||
setInputStatus('error');
|
||||
onError(true);
|
||||
setInputErrorMessage('Please enter time less than 6 months');
|
||||
@ -140,19 +183,25 @@ function CustomTimePicker({
|
||||
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 = (
|
||||
<div className="time-selection-dropdown-content">
|
||||
<div className="time-options-container">
|
||||
{items.map(({ value, label }) => (
|
||||
{items?.map(({ value, label }) => (
|
||||
<div
|
||||
onClick={(): void => {
|
||||
onSelect(value);
|
||||
setSelectedTimePlaceholderValue(label);
|
||||
setInputStatus('');
|
||||
onError(false);
|
||||
setInputErrorMessage(null);
|
||||
setInputValue('');
|
||||
hide();
|
||||
handleSelect(label, value);
|
||||
}}
|
||||
key={value}
|
||||
className={cx(
|
||||
@ -175,6 +224,15 @@ function CustomTimePicker({
|
||||
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 (
|
||||
<div className="custom-time-picker">
|
||||
<Popover
|
||||
@ -184,11 +242,27 @@ function CustomTimePicker({
|
||||
)}
|
||||
placement="bottomRight"
|
||||
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}
|
||||
trigger="hover"
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
trigger={['click']}
|
||||
style={{
|
||||
padding: 0,
|
||||
}}
|
||||
@ -236,3 +310,11 @@ function 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';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Dropdown,
|
||||
MenuProps,
|
||||
@ -152,95 +151,100 @@ function ExplorerCard({
|
||||
const saveButtonType = isQueryUpdated ? 'default' : 'primary';
|
||||
const saveButtonIcon = isQueryUpdated ? null : <SaveOutlined />;
|
||||
|
||||
const showSaveView = false;
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExplorerCardHeadContainer size="small">
|
||||
<Row align="middle">
|
||||
<Col span={6}>
|
||||
<Space>
|
||||
<Typography>Query Builder</Typography>
|
||||
<TextToolTip
|
||||
url={ExploreHeaderToolTip.url}
|
||||
text={ExploreHeaderToolTip.text}
|
||||
useFilledIcon={false}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
<OffSetCol span={18}>
|
||||
<Space size="large">
|
||||
{viewsData?.data.data && viewsData?.data.data.length && (
|
||||
<Space>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
loading={isLoading || isRefetching}
|
||||
showSearch
|
||||
placeholder="Select a view"
|
||||
dropdownStyle={DropDownOverlay}
|
||||
dropdownMatchSelectWidth={false}
|
||||
optionLabelProp="value"
|
||||
value={viewName || undefined}
|
||||
{showSaveView && (
|
||||
<ExplorerCardHeadContainer size="small">
|
||||
<Row align="middle">
|
||||
<Col span={6}>
|
||||
<Space>
|
||||
<Typography>Query Builder</Typography>
|
||||
<TextToolTip
|
||||
url={ExploreHeaderToolTip.url}
|
||||
text={ExploreHeaderToolTip.text}
|
||||
useFilledIcon={false}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
<OffSetCol span={18}>
|
||||
<Space size="large">
|
||||
{viewsData?.data.data && viewsData?.data.data.length && (
|
||||
<Space>
|
||||
<Select
|
||||
getPopupContainer={popupContainer}
|
||||
loading={isLoading || isRefetching}
|
||||
showSearch
|
||||
placeholder="Select a view"
|
||||
dropdownStyle={DropDownOverlay}
|
||||
dropdownMatchSelectWidth={false}
|
||||
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) => (
|
||||
<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}
|
||||
Save changes
|
||||
</Button>
|
||||
)}
|
||||
<Popover
|
||||
getPopupContainer={popupContainer}
|
||||
placement="bottomLeft"
|
||||
trigger="click"
|
||||
content={
|
||||
<SaveViewWithName
|
||||
sourcePage={sourcepage}
|
||||
handlePopOverClose={handleOpenChange}
|
||||
refetchAllView={refetchAllView}
|
||||
/>
|
||||
}
|
||||
showArrow={false}
|
||||
open={isOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
)}
|
||||
<Popover
|
||||
getPopupContainer={popupContainer}
|
||||
placement="bottomLeft"
|
||||
trigger="click"
|
||||
content={
|
||||
<SaveViewWithName
|
||||
sourcePage={sourcepage}
|
||||
handlePopOverClose={handleOpenChange}
|
||||
refetchAllView={refetchAllView}
|
||||
/>
|
||||
}
|
||||
showArrow={false}
|
||||
open={isOpen}
|
||||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
<Button
|
||||
type={saveButtonType}
|
||||
icon={saveButtonIcon}
|
||||
data-testid="traces-save-view-action"
|
||||
>
|
||||
{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>
|
||||
<Button
|
||||
type={saveButtonType}
|
||||
icon={saveButtonIcon}
|
||||
data-testid="traces-save-view-action"
|
||||
>
|
||||
{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>
|
||||
)}
|
||||
|
||||
<div>{children}</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import styled, { CSSProperties } from 'styled-components';
|
||||
|
||||
export const ExplorerCardHeadContainer = styled(Card)`
|
||||
margin: 1rem 0;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
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 MockQueryClientProvider from 'providers/test/MockQueryClientProvider';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@ -46,7 +46,7 @@ describe('ExplorerCard', () => {
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.getByText('Query Builder')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Query Builder')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a save view button', () => {
|
||||
@ -55,19 +55,6 @@ describe('ExplorerCard', () => {
|
||||
<ExplorerCard sourcepage={DataSource.TRACES}>child</ExplorerCard>
|
||||
</MockQueryClientProvider>,
|
||||
);
|
||||
expect(screen.getByText('Save view')).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();
|
||||
});
|
||||
expect(screen.queryByText('Save view')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
SaveViewHandlerProps,
|
||||
} from './types';
|
||||
|
||||
const showErrorNotification = (
|
||||
export const showErrorNotification = (
|
||||
notifications: NotificationInstance,
|
||||
err: Error,
|
||||
): void => {
|
||||
@ -90,6 +90,14 @@ export const isQueryUpdatedInView = ({
|
||||
// Omitting id from aggregateAttribute and groupBy
|
||||
const updatedCurrentQuery = omitIdFromQuery(stagedQuery);
|
||||
|
||||
if (
|
||||
updatedCurrentQuery?.builder === undefined ||
|
||||
updatedCurrentQuery.clickhouse_sql === undefined ||
|
||||
updatedCurrentQuery.promql === undefined
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
panelType !== currentPanelType ||
|
||||
!isEqual(query.builder, updatedCurrentQuery?.builder) ||
|
||||
|
@ -3,8 +3,11 @@ import { AddToQueryHOCProps } from 'components/Logs/AddToQueryHOC';
|
||||
import { ActionItemProps } from 'container/LogDetailedView/ActionItem';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { VIEWS } from './constants';
|
||||
|
||||
export type LogDetailProps = {
|
||||
log: ILog | null;
|
||||
selectedTab: VIEWS;
|
||||
} & Pick<AddToQueryHOCProps, 'onAddToQuery'> &
|
||||
Pick<ActionItemProps, 'onClickActionItem'> &
|
||||
Partial<Pick<ActionItemProps, 'onClickActionItem'>> &
|
||||
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';
|
||||
import JSONView from 'container/LogDetailedView/JsonView';
|
||||
import TableView from 'container/LogDetailedView/TableView';
|
||||
import { useMemo } from 'react';
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import './LogDetails.styles.scss';
|
||||
|
||||
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 QueryBuilderSearchWrapper from './QueryBuilderSearchWrapper';
|
||||
|
||||
function LogDetail({
|
||||
log,
|
||||
onClose,
|
||||
onAddToQuery,
|
||||
onClickActionItem,
|
||||
selectedTab,
|
||||
}: LogDetailProps): JSX.Element {
|
||||
const items = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Table',
|
||||
key: '1',
|
||||
children: log && (
|
||||
<TableView
|
||||
logData={log}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onClickActionItem}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'JSON',
|
||||
key: '2',
|
||||
children: log && <JSONView logData={log} />,
|
||||
},
|
||||
],
|
||||
[log, onAddToQuery, onClickActionItem],
|
||||
);
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const [selectedView, setSelectedView] = useState<VIEWS>(selectedTab);
|
||||
|
||||
const [isFilterVisibile, setIsFilterVisible] = useState<boolean>(false);
|
||||
|
||||
const [contextQuery, setContextQuery] = useState<Query | undefined>();
|
||||
const [filters, setFilters] = useState<TagFilter | null>(null);
|
||||
const [isEdit, setIsEdit] = useState<boolean>(false);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const LogJsonData = log ? aggregateAttributesResourcesToString(log) : '';
|
||||
|
||||
const handleModeChange = (e: RadioChangeEvent): void => {
|
||||
setSelectedView(e.target.value);
|
||||
setIsEdit(false);
|
||||
setIsFilterVisible(false);
|
||||
};
|
||||
|
||||
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 (
|
||||
<Drawer
|
||||
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"
|
||||
closable
|
||||
onClose={onClose}
|
||||
// closable
|
||||
onClose={drawerCloseHandler}
|
||||
open={log !== null}
|
||||
style={{ overscrollBehavior: 'contain' }}
|
||||
style={{
|
||||
overscrollBehavior: 'contain',
|
||||
background: isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100,
|
||||
}}
|
||||
className="log-detail-drawer"
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
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 { OPERATORS } from 'constants/queryBuilder';
|
||||
import { memo, ReactNode, useCallback, useMemo } from 'react';
|
||||
|
||||
import { ButtonContainer } from './styles';
|
||||
|
||||
function AddToQueryHOC({
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
@ -19,11 +19,12 @@ function AddToQueryHOC({
|
||||
]);
|
||||
|
||||
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}>
|
||||
{children}
|
||||
</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 {
|
||||
CopyFilled,
|
||||
ExpandAltOutlined,
|
||||
LinkOutlined,
|
||||
MonitorOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import './ListLogView.styles.scss';
|
||||
|
||||
import { blue } from '@ant-design/colors';
|
||||
import Convert from 'ansi-to-html';
|
||||
import { Button, Divider, Row, Typography } from 'antd';
|
||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||
import { Typography } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
// utils
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
// interfaces
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
// components
|
||||
import AddToQueryHOC, { AddToQueryHOCProps } from '../AddToQueryHOC';
|
||||
import CopyClipboardHOC from '../CopyClipboardHOC';
|
||||
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
||||
import LogStateIndicator, {
|
||||
LogType,
|
||||
} from '../LogStateIndicator/LogStateIndicator';
|
||||
// styles
|
||||
import {
|
||||
Container,
|
||||
LogContainer,
|
||||
LogText,
|
||||
SelectedLog,
|
||||
Text,
|
||||
TextContainer,
|
||||
} from './styles';
|
||||
@ -55,12 +52,10 @@ function LogGeneralField({ fieldKey, fieldValue }: LogFieldProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<TextContainer>
|
||||
<Text ellipsis type="secondary">
|
||||
{`${fieldKey}: `}
|
||||
<Text ellipsis type="secondary" className="log-field-key">
|
||||
{`${fieldKey} : `}
|
||||
</Text>
|
||||
<CopyClipboardHOC textToCopy={fieldValue}>
|
||||
<LogText dangerouslySetInnerHTML={html} />
|
||||
</CopyClipboardHOC>
|
||||
<LogText dangerouslySetInnerHTML={html} className="log-value" />
|
||||
</TextContainer>
|
||||
);
|
||||
}
|
||||
@ -71,23 +66,23 @@ function LogSelectedField({
|
||||
onAddToQuery,
|
||||
}: LogSelectedFieldProps): JSX.Element {
|
||||
return (
|
||||
<SelectedLog>
|
||||
<div className="log-selected-fields">
|
||||
<AddToQueryHOC
|
||||
fieldKey={fieldKey}
|
||||
fieldValue={fieldValue}
|
||||
onAddToQuery={onAddToQuery}
|
||||
>
|
||||
<Typography.Text>
|
||||
<span style={{ color: blue[4] }}>{fieldKey}</span>
|
||||
<span style={{ color: blue[4] }} className="selected-log-field-key">
|
||||
{fieldKey}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
</AddToQueryHOC>
|
||||
<CopyClipboardHOC textToCopy={fieldValue}>
|
||||
<Typography.Text ellipsis>
|
||||
<span>{': '}</span>
|
||||
<span style={{ color: orange[6] }}>{fieldValue || "''"}</span>
|
||||
</Typography.Text>
|
||||
</CopyClipboardHOC>
|
||||
</SelectedLog>
|
||||
<Typography.Text ellipsis className="selected-log-kv">
|
||||
<span className="selected-log-field-key">{': '}</span>
|
||||
<span className="selected-log-value">{fieldValue || "''"}</span>
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -96,6 +91,7 @@ type ListLogViewProps = {
|
||||
selectedFields: IField[];
|
||||
onSetActiveLog: (log: ILog) => void;
|
||||
onAddToQuery: AddToQueryHOCProps['onAddToQuery'];
|
||||
activeLog?: ILog | null;
|
||||
};
|
||||
|
||||
function ListLogView({
|
||||
@ -103,34 +99,42 @@ function ListLogView({
|
||||
selectedFields,
|
||||
onSetActiveLog,
|
||||
onAddToQuery,
|
||||
activeLog,
|
||||
}: ListLogViewProps): JSX.Element {
|
||||
const flattenLogData = useMemo(() => FlatLogData(logData), [logData]);
|
||||
|
||||
const [, setCopy] = useCopyToClipboard();
|
||||
const { notifications } = useNotifications();
|
||||
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
|
||||
const { isHighlighted, isLogsExplorerPage, onLogCopy } = useCopyLogLink(
|
||||
logData.id,
|
||||
);
|
||||
const {
|
||||
activeLog: activeContextLog,
|
||||
onAddToQuery: handleAddToQuery,
|
||||
onSetActiveLog: handleSetActiveContextLog,
|
||||
onClearActiveLog: handleClearActiveContextLog,
|
||||
} = useActiveLog();
|
||||
|
||||
const handlerClearActiveContextLog = useCallback(
|
||||
(event: React.MouseEvent | React.KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleClearActiveContextLog();
|
||||
},
|
||||
[handleClearActiveContextLog],
|
||||
);
|
||||
|
||||
const handleDetailedView = useCallback(() => {
|
||||
onSetActiveLog(logData);
|
||||
}, [logData, onSetActiveLog]);
|
||||
|
||||
const handleShowContext = useCallback(() => {
|
||||
handleSetActiveContextLog(logData);
|
||||
}, [logData, handleSetActiveContextLog]);
|
||||
|
||||
const handleCopyJSON = (): void => {
|
||||
setCopy(JSON.stringify(logData, null, 2));
|
||||
notifications.success({
|
||||
message: 'Copied to clipboard',
|
||||
});
|
||||
};
|
||||
const handleShowContext = useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleSetActiveContextLog(logData);
|
||||
},
|
||||
[logData, handleSetActiveContextLog],
|
||||
);
|
||||
|
||||
const updatedSelecedFields = useMemo(
|
||||
() => selectedFields.filter((e) => e.name !== 'id'),
|
||||
@ -145,84 +149,74 @@ function ListLogView({
|
||||
[flattenLogData.timestamp],
|
||||
);
|
||||
|
||||
const logType = logData?.attributes_string?.log_level || LogType.INFO;
|
||||
|
||||
const handleMouseEnter = (): void => {
|
||||
setHasActionButtons(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = (): void => {
|
||||
setHasActionButtons(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container $isActiveLog={isHighlighted}>
|
||||
<div>
|
||||
<LogContainer>
|
||||
<>
|
||||
<LogGeneralField fieldKey="log" fieldValue={flattenLogData.body} />
|
||||
{flattenLogData.stream && (
|
||||
<LogGeneralField fieldKey="stream" fieldValue={flattenLogData.stream} />
|
||||
)}
|
||||
<LogGeneralField fieldKey="timestamp" fieldValue={timestampValue} />
|
||||
</>
|
||||
</LogContainer>
|
||||
<div>
|
||||
{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,
|
||||
)}
|
||||
<>
|
||||
<Container
|
||||
$isActiveLog={isHighlighted}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleDetailedView}
|
||||
>
|
||||
<div className="log-line">
|
||||
<LogStateIndicator
|
||||
type={logType}
|
||||
isActive={
|
||||
activeLog?.id === logData.id || activeContextLog?.id === logData.id
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<LogContainer>
|
||||
<LogGeneralField fieldKey="Log" fieldValue={flattenLogData.body} />
|
||||
{flattenLogData.stream && (
|
||||
<LogGeneralField fieldKey="Stream" fieldValue={flattenLogData.stream} />
|
||||
)}
|
||||
<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>
|
||||
<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 && (
|
||||
<>
|
||||
<Button
|
||||
size="small"
|
||||
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}
|
||||
{hasActionButtons && isLogsExplorerPage && (
|
||||
<LogLinesActionButtons
|
||||
handleShowContext={handleShowContext}
|
||||
onLogCopy={onLogCopy}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</Container>
|
||||
</Container>
|
||||
{activeContextLog && (
|
||||
<LogDetail
|
||||
log={activeContextLog}
|
||||
onAddToQuery={handleAddToQuery}
|
||||
selectedTab={VIEW_TYPES.CONTEXT}
|
||||
onClose={handlerClearActiveContextLog}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ListLogView.defaultProps = {
|
||||
activeLog: null,
|
||||
};
|
||||
|
||||
export default ListLogView;
|
||||
|
@ -7,6 +7,7 @@ export const Container = styled(Card)<{
|
||||
}>`
|
||||
width: 100% !important;
|
||||
margin-bottom: 0.3rem;
|
||||
cursor: pointer;
|
||||
.ant-card-body {
|
||||
padding: 0.3rem 0.6rem;
|
||||
}
|
||||
@ -29,11 +30,13 @@ export const TextContainer = styled.div`
|
||||
|
||||
export const LogContainer = styled.div`
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
`;
|
||||
|
||||
export const LogText = styled.div`
|
||||
display: inline-block;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
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 {
|
||||
ExpandAltOutlined,
|
||||
LinkOutlined,
|
||||
MonitorOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import './RawLogView.styles.scss';
|
||||
|
||||
import Convert from 'ansi-to-html';
|
||||
import { Button, DrawerProps, Tooltip } from 'antd';
|
||||
import { DrawerProps } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES, VIEWS } from 'components/LogDetail/constants';
|
||||
import LogsExplorerContext from 'container/LogsExplorerContext';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
@ -14,7 +12,7 @@ import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
// hooks
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { isEmpty, isUndefined } from 'lodash-es';
|
||||
import { isEmpty, isNumber, isUndefined } from 'lodash-es';
|
||||
import {
|
||||
KeyboardEvent,
|
||||
MouseEvent,
|
||||
@ -24,13 +22,12 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import LogLinesActionButtons from '../LogLinesActionButtons/LogLinesActionButtons';
|
||||
import LogStateIndicator, {
|
||||
LogType,
|
||||
} from '../LogStateIndicator/LogStateIndicator';
|
||||
// styles
|
||||
import {
|
||||
ActionButtonsWrapper,
|
||||
ExpandIconWrapper,
|
||||
RawLogContent,
|
||||
RawLogViewContainer,
|
||||
} from './styles';
|
||||
import { RawLogContent, RawLogViewContainer } from './styles';
|
||||
import { RawLogViewProps } from './types';
|
||||
|
||||
const convert = new Convert();
|
||||
@ -50,7 +47,6 @@ function RawLogView({
|
||||
|
||||
const {
|
||||
activeLog: activeContextLog,
|
||||
onSetActiveLog: handleSetActiveContextLog,
|
||||
onClearActiveLog: handleClearActiveContextLog,
|
||||
} = useActiveLog();
|
||||
const {
|
||||
@ -61,12 +57,15 @@ function RawLogView({
|
||||
} = useActiveLog();
|
||||
|
||||
const [hasActionButtons, setHasActionButtons] = useState<boolean>(false);
|
||||
const [selectedTab, setSelectedTab] = useState<VIEWS | undefined>();
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const isReadOnlyLog = !isLogsExplorerPage || isReadOnly;
|
||||
|
||||
const severityText = data.severity_text ? `${data.severity_text} |` : '';
|
||||
|
||||
const logType = data?.attributes_string?.log_level || LogType.INFO;
|
||||
|
||||
const updatedSelecedFields = useMemo(
|
||||
() => selectedFields.filter((e) => e.name !== 'id'),
|
||||
[selectedFields],
|
||||
@ -74,7 +73,14 @@ function RawLogView({
|
||||
|
||||
const attributesValues = updatedSelecedFields
|
||||
.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(' | ');
|
||||
|
||||
@ -98,6 +104,7 @@ function RawLogView({
|
||||
if (activeContextLog || isReadOnly) return;
|
||||
|
||||
onSetActiveLog(data);
|
||||
setSelectedTab(VIEW_TYPES.OVERVIEW);
|
||||
}, [activeContextLog, isReadOnly, data, onSetActiveLog]);
|
||||
|
||||
const handleCloseLogDetail: DrawerProps['onClose'] = useCallback(
|
||||
@ -108,6 +115,7 @@ function RawLogView({
|
||||
event.stopPropagation();
|
||||
|
||||
onClearActiveLog();
|
||||
setSelectedTab(undefined);
|
||||
},
|
||||
[onClearActiveLog],
|
||||
);
|
||||
@ -128,9 +136,11 @@ function RawLogView({
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleSetActiveContextLog(data);
|
||||
// handleSetActiveContextLog(data);
|
||||
setSelectedTab(VIEW_TYPES.CONTEXT);
|
||||
onSetActiveLog(data);
|
||||
},
|
||||
[data, handleSetActiveContextLog],
|
||||
[data, onSetActiveLog],
|
||||
);
|
||||
|
||||
const html = useMemo(
|
||||
@ -147,37 +157,30 @@ function RawLogView({
|
||||
align="middle"
|
||||
$isDarkMode={isDarkMode}
|
||||
$isReadOnly={isReadOnly}
|
||||
$isActiveLog={isHighlighted}
|
||||
$isHightlightedLog={isHighlighted}
|
||||
$isActiveLog={isActiveLog}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
{!isReadOnly && (
|
||||
<ExpandIconWrapper flex="30px">
|
||||
<ExpandAltOutlined />
|
||||
</ExpandIconWrapper>
|
||||
)}
|
||||
<LogStateIndicator
|
||||
type={logType}
|
||||
isActive={activeLog?.id === data.id || activeContextLog?.id === data.id}
|
||||
/>
|
||||
|
||||
<RawLogContent
|
||||
$isReadOnly={isReadOnly}
|
||||
$isActiveLog={isActiveLog}
|
||||
$isDarkMode={isDarkMode}
|
||||
$isTextOverflowEllipsisDisabled={isTextOverflowEllipsisDisabled}
|
||||
linesPerRow={linesPerRow}
|
||||
dangerouslySetInnerHTML={html}
|
||||
/>
|
||||
|
||||
{hasActionButtons && (
|
||||
<ActionButtonsWrapper>
|
||||
<Tooltip title="Show Context">
|
||||
<Button
|
||||
size="small"
|
||||
icon={<MonitorOutlined />}
|
||||
onClick={handleShowContext}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Copy Link">
|
||||
<Button size="small" icon={<LinkOutlined />} onClick={onLogCopy} />
|
||||
</Tooltip>
|
||||
</ActionButtonsWrapper>
|
||||
<LogLinesActionButtons
|
||||
handleShowContext={handleShowContext}
|
||||
onLogCopy={onLogCopy}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeContextLog && (
|
||||
@ -186,12 +189,15 @@ function RawLogView({
|
||||
onClose={handleClearActiveContextLog}
|
||||
/>
|
||||
)}
|
||||
<LogDetail
|
||||
log={activeLog}
|
||||
onClose={handleCloseLogDetail}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
{selectedTab && (
|
||||
<LogDetail
|
||||
selectedTab={selectedTab}
|
||||
log={activeLog}
|
||||
onClose={handleCloseLogDetail}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
/>
|
||||
)}
|
||||
</RawLogViewContainer>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { blue } from '@ant-design/colors';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Col, Row, Space } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { getActiveLogBackground, getDefaultLogBackground } from 'utils/logs';
|
||||
@ -9,20 +10,25 @@ export const RawLogViewContainer = styled(Row)<{
|
||||
$isDarkMode: boolean;
|
||||
$isReadOnly?: boolean;
|
||||
$isActiveLog?: boolean;
|
||||
$isHightlightedLog: boolean;
|
||||
}>`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
font-weight: 700;
|
||||
font-size: 0.625rem;
|
||||
line-height: 1.25rem;
|
||||
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
transition: background-color 0.2s ease-in;
|
||||
|
||||
.log-state-indicator {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
${({ $isActiveLog }): string => getActiveLogBackground($isActiveLog)}
|
||||
|
||||
${({ $isReadOnly, $isDarkMode, $isActiveLog }): string =>
|
||||
${({ $isReadOnly, $isActiveLog, $isDarkMode }): string =>
|
||||
$isActiveLog
|
||||
? getActiveLogBackground()
|
||||
? getActiveLogBackground($isActiveLog, $isDarkMode)
|
||||
: getDefaultLogBackground($isReadOnly, $isDarkMode)}
|
||||
`;
|
||||
|
||||
@ -30,13 +36,17 @@ export const ExpandIconWrapper = styled(Col)`
|
||||
color: ${blue[6]};
|
||||
padding: 0.25rem 0.375rem;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
`;
|
||||
|
||||
export const RawLogContent = styled.div<RawLogContentProps>`
|
||||
margin-bottom: 0;
|
||||
font-family: Fira Code, monospace;
|
||||
font-weight: 300;
|
||||
font-family: 'SF Mono', monospace;
|
||||
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
|
||||
@ -48,15 +58,12 @@ export const RawLogContent = styled.div<RawLogContentProps>`
|
||||
line-clamp: ${linesPerRow};
|
||||
-webkit-box-orient: vertical;`};
|
||||
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
letter-spacing: -0.07px;
|
||||
padding: 4px;
|
||||
|
||||
cursor: ${({ $isActiveLog, $isReadOnly }): string =>
|
||||
$isActiveLog || $isReadOnly ? 'initial' : 'pointer'};
|
||||
|
||||
${({ $isActiveLog, $isReadOnly }): string =>
|
||||
$isReadOnly && $isActiveLog ? 'padding: 0 1.5rem;' : ''}
|
||||
`;
|
||||
|
||||
export const ActionButtonsWrapper = styled(Space)`
|
||||
|
@ -14,5 +14,6 @@ export interface RawLogContentProps {
|
||||
linesPerRow: number;
|
||||
$isReadOnly?: boolean;
|
||||
$isActiveLog?: boolean;
|
||||
$isDarkMode?: boolean;
|
||||
$isTextOverflowEllipsisDisabled?: boolean;
|
||||
}
|
||||
|
@ -1,12 +1,21 @@
|
||||
import { TableProps } from 'antd';
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
export const defaultCellStyle: CSSProperties = {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 6,
|
||||
paddingRight: 8,
|
||||
paddingLeft: 8,
|
||||
};
|
||||
export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
|
||||
return {
|
||||
paddingTop: 4,
|
||||
paddingBottom: 6,
|
||||
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 = {
|
||||
minWidth: '40rem',
|
||||
|
@ -2,18 +2,22 @@ import styled from 'styled-components';
|
||||
|
||||
interface TableBodyContentProps {
|
||||
linesPerRow: number;
|
||||
isDarkMode?: boolean;
|
||||
}
|
||||
|
||||
export const TableBodyContent = styled.div<TableBodyContentProps>`
|
||||
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;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: ${(props): number => props.linesPerRow};
|
||||
line-clamp: ${(props): number => props.linesPerRow};
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
font-size: 0.875rem;
|
||||
|
||||
line-height: 2rem;
|
||||
`;
|
||||
|
@ -22,6 +22,8 @@ export type UseTableViewProps = {
|
||||
appendTo?: 'center' | 'end';
|
||||
onOpenLogsContext?: (log: ILog) => void;
|
||||
onClickExpand?: (log: ILog) => void;
|
||||
activeLog?: ILog | null;
|
||||
activeContextLog?: ILog | null;
|
||||
} & LogsTableViewProps;
|
||||
|
||||
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 {
|
||||
ExpandAltOutlined,
|
||||
LinkOutlined,
|
||||
MonitorOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import './useTableView.styles.scss';
|
||||
|
||||
import Convert from 'ansi-to-html';
|
||||
import { Button, Space, Typography } from 'antd';
|
||||
import { Typography } from 'antd';
|
||||
import { ColumnsType } from 'antd/es/table';
|
||||
import dayjs from 'dayjs';
|
||||
import dompurify from 'dompurify';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
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 { defaultCellStyle, defaultTableStyle } from './config';
|
||||
import LogStateIndicator, {
|
||||
LogType,
|
||||
} from '../LogStateIndicator/LogStateIndicator';
|
||||
import { defaultTableStyle, getDefaultCellStyle } from './config';
|
||||
import { TableBodyContent } from './styles';
|
||||
import {
|
||||
ActionsColumnProps,
|
||||
ColumnTypeRender,
|
||||
UseTableViewProps,
|
||||
UseTableViewResult,
|
||||
@ -24,60 +23,22 @@ import {
|
||||
|
||||
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 => {
|
||||
const {
|
||||
logs,
|
||||
fields,
|
||||
linesPerRow,
|
||||
appendTo = 'center',
|
||||
onOpenLogsContext,
|
||||
onClickExpand,
|
||||
activeContextLog,
|
||||
activeLog,
|
||||
} = props;
|
||||
const { isLogsExplorerPage } = useCopyLogLink();
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const flattenLogData = useMemo(() => logs.map((log) => FlatLogData(log)), [
|
||||
logs,
|
||||
]);
|
||||
|
||||
const handleClickExpand = useCallback(
|
||||
(index: number): void => {
|
||||
if (!onClickExpand) return;
|
||||
|
||||
onClickExpand(logs[index]);
|
||||
},
|
||||
[logs, onClickExpand],
|
||||
);
|
||||
|
||||
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
|
||||
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
|
||||
.filter((e) => e.name !== 'id')
|
||||
@ -87,7 +48,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
key: name,
|
||||
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
|
||||
props: {
|
||||
style: defaultCellStyle,
|
||||
style: getDefaultCellStyle(isDarkMode),
|
||||
},
|
||||
children: (
|
||||
<Typography.Paragraph ellipsis={{ rows: linesPerRow }}>
|
||||
@ -98,38 +59,30 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
|
||||
}));
|
||||
|
||||
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',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
// https://github.com/ant-design/ant-design/discussions/36886
|
||||
render: (field): ColumnTypeRender<Record<string, unknown>> => {
|
||||
render: (field, item): ColumnTypeRender<Record<string, unknown>> => {
|
||||
const date =
|
||||
typeof field === 'string'
|
||||
? dayjs(field).format()
|
||||
: dayjs(field / 1e6).format();
|
||||
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)),
|
||||
}}
|
||||
linesPerRow={linesPerRow}
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
},
|
||||
...(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,
|
||||
appendTo,
|
||||
isDarkMode,
|
||||
linesPerRow,
|
||||
isLogsExplorerPage,
|
||||
handleClickExpand,
|
||||
onOpenLogsContext,
|
||||
activeLog?.id,
|
||||
activeContextLog?.id,
|
||||
]);
|
||||
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
const allowedPaths = [
|
||||
const allowedPaths: string[] = [
|
||||
ROUTES.LIST_ALL_ALERT,
|
||||
ROUTES.APPLICATION,
|
||||
ROUTES.ALL_DASHBOARD,
|
||||
];
|
||||
|
||||
return (
|
||||
userFlags?.ReleaseNote0120Hide !== 'Y' &&
|
||||
allowedPaths.includes(path) &&
|
||||
|
@ -1,25 +1,31 @@
|
||||
.DynamicColumnTable {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
.dynamicColumnTable-button {
|
||||
align-self: flex-end;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.dynamicColumnTable-button {
|
||||
align-self: flex-end;
|
||||
margin: 10px 0;
|
||||
|
||||
&.filter-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dynamicColumnsTable-items {
|
||||
display: flex;
|
||||
width: 10.625rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
width: 10.625rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dynamicColumnsTable-items {
|
||||
flex-direction: column;
|
||||
width: auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.dynamicColumnsTable-items {
|
||||
flex-direction: column;
|
||||
width: auto;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
import './DynamicColumnTable.syles.scss';
|
||||
|
||||
import { SettingOutlined } from '@ant-design/icons';
|
||||
import { Button, Dropdown, MenuProps, Switch } from 'antd';
|
||||
import { ColumnsType } from 'antd/lib/table';
|
||||
import { SlidersHorizontal } from 'lucide-react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
@ -90,9 +90,9 @@ function DynamicColumnTable({
|
||||
trigger={['click']}
|
||||
>
|
||||
<Button
|
||||
className="dynamicColumnTable-button"
|
||||
className="dynamicColumnTable-button filter-btn"
|
||||
size="middle"
|
||||
icon={<SettingOutlined />}
|
||||
icon={<SlidersHorizontal size={14} />}
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { TabsProps } from 'antd';
|
||||
import { History } from 'history';
|
||||
|
||||
export type TabRoutes = {
|
||||
name: React.ReactNode;
|
||||
route: string;
|
||||
Component: () => JSX.Element;
|
||||
key: string;
|
||||
};
|
||||
|
||||
export interface RouteTabProps {
|
||||
routes: {
|
||||
name: React.ReactNode;
|
||||
route: string;
|
||||
Component: () => JSX.Element;
|
||||
key: string;
|
||||
}[];
|
||||
routes: TabRoutes[];
|
||||
activeKey: TabsProps['activeKey'];
|
||||
onChangeHandler?: VoidFunction;
|
||||
history: History<unknown>;
|
||||
|
@ -15,4 +15,5 @@ export enum LOCALSTORAGE {
|
||||
LOGGED_IN_USER_EMAIL = 'LOGGED_IN_USER_EMAIL',
|
||||
CHAT_SUPPORT = 'CHAT_SUPPORT',
|
||||
IS_IDENTIFIED_USER = 'IS_IDENTIFIED_USER',
|
||||
DASHBOARD_VARIABLES = 'DASHBOARD_VARIABLES',
|
||||
}
|
||||
|
@ -41,7 +41,10 @@ const ROUTES = {
|
||||
TRACE_EXPLORER: '/trace-explorer',
|
||||
BILLING: '/billing',
|
||||
SUPPORT: '/support',
|
||||
LOGS_SAVE_VIEWS: '/logs-save-views',
|
||||
TRACES_SAVE_VIEWS: '/traces-save-views',
|
||||
WORKSPACE_LOCKED: '/workspace-locked',
|
||||
};
|
||||
SHORTCUTS: '/shortcuts',
|
||||
} as const;
|
||||
|
||||
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 {
|
||||
|
@ -1,12 +1,20 @@
|
||||
@import '@signozhq/design-tokens';
|
||||
|
||||
.app-layout {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.app-content {
|
||||
width: 100%;
|
||||
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 { Flex } from 'antd';
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import getDynamicConfigs from 'api/dynamicConfigs/getDynamicConfigs';
|
||||
import getUserLatestVersion from 'api/user/getLatestVersion';
|
||||
import getUserVersion from 'api/user/getVersion';
|
||||
import cx from 'classnames';
|
||||
import { IS_SIDEBAR_COLLAPSED } from 'constants/app';
|
||||
import ROUTES from 'constants/routes';
|
||||
import SideNav from 'container/SideNav';
|
||||
import TopNav from 'container/TopNav';
|
||||
@ -16,7 +18,15 @@ import useLicense from 'hooks/useLicense';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import history from 'lib/history';
|
||||
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 { Helmet } from 'react-helmet-async';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -24,6 +34,7 @@ import { useQueries } from 'react-query';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Dispatch } from 'redux';
|
||||
import { sideBarCollapse } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import AppActions from 'types/actions';
|
||||
import {
|
||||
@ -44,6 +55,10 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
(state) => state.app,
|
||||
);
|
||||
|
||||
const [collapsed, setCollapsed] = useState<boolean>(
|
||||
getLocalStorageKey(IS_SIDEBAR_COLLAPSED) === 'true',
|
||||
);
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const { data: licenseData, isFetching } = useLicense();
|
||||
@ -92,7 +107,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
const { children } = props;
|
||||
|
||||
const dispatch = useDispatch<Dispatch<AppActions>>();
|
||||
const dispatch = useDispatch<Dispatch<AppActions | any>>();
|
||||
|
||||
const latestCurrentCounter = useRef(0);
|
||||
const latestVersionCounter = useRef(0);
|
||||
@ -100,6 +115,14 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
const onCollapse = useCallback(() => {
|
||||
setCollapsed((collapsed) => !collapsed);
|
||||
}, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
dispatch(sideBarCollapse(collapsed));
|
||||
}, [collapsed, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
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 (
|
||||
<Layout className={isDarkMode ? 'darkMode' : 'lightMode'}>
|
||||
<Layout
|
||||
className={cx(
|
||||
isDarkMode ? 'darkMode' : 'lightMode',
|
||||
isSideNavCollapsed ? 'sidebarCollapsed' : '',
|
||||
)}
|
||||
>
|
||||
<Helmet>
|
||||
<title>{pageTitle}</title>
|
||||
</Helmet>
|
||||
@ -259,12 +308,21 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
|
||||
|
||||
<Flex className={cx('app-layout', isDarkMode ? 'darkMode' : 'lightMode')}>
|
||||
{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}>
|
||||
<LayoutContent>
|
||||
<ChildrenContainer>
|
||||
<ChildrenContainer
|
||||
style={{
|
||||
margin: isLogsView() || isTracesView() ? 0 : ' 0 1rem',
|
||||
}}
|
||||
>
|
||||
{isToDisplayLayout && !renderFullScreen && <TopNav />}
|
||||
{children}
|
||||
</ChildrenContainer>
|
||||
|
@ -18,7 +18,6 @@ export const LayoutContent = styled(LayoutComponent.Content)`
|
||||
`;
|
||||
|
||||
export const ChildrenContainer = styled.div`
|
||||
margin: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
@ -7,16 +7,18 @@ function EditRules({ initialValue, ruleId }: EditRulesProps): JSX.Element {
|
||||
const [formInstance] = Form.useForm();
|
||||
|
||||
return (
|
||||
<FormAlertRules
|
||||
alertType={
|
||||
initialValue.alertType
|
||||
? (initialValue.alertType as AlertTypes)
|
||||
: AlertTypes.METRICS_BASED_ALERT
|
||||
}
|
||||
formInstance={formInstance}
|
||||
initialValue={initialValue}
|
||||
ruleId={ruleId}
|
||||
/>
|
||||
<div style={{ marginTop: '1rem' }}>
|
||||
<FormAlertRules
|
||||
alertType={
|
||||
initialValue.alertType
|
||||
? (initialValue.alertType as AlertTypes)
|
||||
: AlertTypes.METRICS_BASED_ALERT
|
||||
}
|
||||
formInstance={formInstance}
|
||||
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';
|
||||
import { filterOptions, getSelectOptions } from './utils';
|
||||
|
||||
function ExportPanel({ isLoading, onExport }: ExportPanelProps): JSX.Element {
|
||||
function ExportPanelContainer({
|
||||
isLoading,
|
||||
onExport,
|
||||
}: ExportPanelProps): JSX.Element {
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
|
||||
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 { Button, Modal, Space } from 'antd';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
import { Modal } from 'antd';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import ExportPanelContainer from './ExportPanel';
|
||||
import ExportPanelContainer from './ExportPanelContainer';
|
||||
|
||||
function ExportPanel({
|
||||
isLoading,
|
||||
@ -20,53 +16,25 @@ function ExportPanel({
|
||||
setIsExport(value);
|
||||
}, []);
|
||||
|
||||
const onCreateAlertsHandler = useCallback(() => {
|
||||
history.push(
|
||||
`${ROUTES.ALERTS_NEW}?${QueryParams.compositeQuery}=${encodeURIComponent(
|
||||
JSON.stringify(query),
|
||||
)}`,
|
||||
);
|
||||
}, [query]);
|
||||
|
||||
const onCancel = (value: boolean) => (): void => {
|
||||
onModalToggle(value);
|
||||
};
|
||||
|
||||
const onAddToDashboard = (): void => {
|
||||
setIsExport(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Space size={24}>
|
||||
<Button
|
||||
icon={<AreaChartOutlined />}
|
||||
onClick={onAddToDashboard}
|
||||
type="primary"
|
||||
>
|
||||
Add to Dashboard
|
||||
</Button>
|
||||
|
||||
<Button onClick={onCreateAlertsHandler} icon={<AlertOutlined />}>
|
||||
Setup Alerts
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Modal
|
||||
footer={null}
|
||||
onOk={onCancel(false)}
|
||||
onCancel={onCancel(false)}
|
||||
open={isExport}
|
||||
centered
|
||||
destroyOnClose
|
||||
>
|
||||
<ExportPanelContainer
|
||||
query={query}
|
||||
isLoading={isLoading}
|
||||
onExport={onExport}
|
||||
/>
|
||||
</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 { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { Time } from 'container/TopNav/DateTimeSelection/config';
|
||||
import { Time as TimeV2 } from 'container/TopNav/DateTimeSelectionV2/config';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
@ -28,7 +29,7 @@ export interface ChartPreviewProps {
|
||||
query: Query | null;
|
||||
graphType?: PANEL_TYPES;
|
||||
selectedTime?: timePreferenceType;
|
||||
selectedInterval?: Time;
|
||||
selectedInterval?: Time | TimeV2;
|
||||
headline?: JSX.Element;
|
||||
alertDef?: AlertDef;
|
||||
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 { PANEL_TYPES } from 'constants/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 { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
@ -22,6 +25,7 @@ function QuerySection({
|
||||
}: QuerySectionProps): JSX.Element {
|
||||
// init namespace for translations
|
||||
const { t } = useTranslation('alerts');
|
||||
const [currentTab, setCurrentTab] = useState(queryCategory);
|
||||
|
||||
const { featureResponse } = useSelector<AppState, AppReducer>(
|
||||
(state) => state.app,
|
||||
@ -31,6 +35,7 @@ function QuerySection({
|
||||
featureResponse.refetch().then(() => {
|
||||
setQueryCategory(queryType as EQueryType);
|
||||
});
|
||||
setCurrentTab(queryType as EQueryType);
|
||||
};
|
||||
|
||||
const renderPromqlUI = (): JSX.Element => <PromqlSection />;
|
||||
@ -49,22 +54,61 @@ function QuerySection({
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
label: t('tab_qb'),
|
||||
label: (
|
||||
<Tooltip title="Query Builder">
|
||||
<Button className="nav-btns">
|
||||
<Atom size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
key: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
{
|
||||
label: t('tab_chquery'),
|
||||
label: (
|
||||
<Tooltip title="ClickHouse">
|
||||
<Button className="nav-btns">
|
||||
<Terminal size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
),
|
||||
key: EQueryType.CLICKHOUSE,
|
||||
},
|
||||
];
|
||||
|
||||
const items = useMemo(
|
||||
() => [
|
||||
{ label: t('tab_qb'), key: EQueryType.QUERY_BUILDER },
|
||||
{ label: t('tab_chquery'), key: EQueryType.CLICKHOUSE },
|
||||
{ label: t('tab_promql'), key: EQueryType.PROM },
|
||||
{
|
||||
label: (
|
||||
<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 => {
|
||||
@ -73,40 +117,54 @@ function QuerySection({
|
||||
case AlertTypes.LOGS_BASED_ALERT:
|
||||
case AlertTypes.EXCEPTIONS_BASED_ALERT:
|
||||
return (
|
||||
<Tabs
|
||||
type="card"
|
||||
style={{ width: '100%' }}
|
||||
defaultActiveKey={EQueryType.QUERY_BUILDER}
|
||||
activeKey={queryCategory}
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<Button type="primary" onClick={runQuery}>
|
||||
Run Query
|
||||
</Button>
|
||||
</span>
|
||||
}
|
||||
items={tabs}
|
||||
/>
|
||||
<div className="alert-tabs">
|
||||
<Tabs
|
||||
type="card"
|
||||
style={{ width: '100%' }}
|
||||
defaultActiveKey={currentTab}
|
||||
activeKey={currentTab}
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={runQuery}
|
||||
className="stage-run-query"
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
Stage & Run Query
|
||||
</Button>
|
||||
</span>
|
||||
}
|
||||
items={tabs}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case AlertTypes.METRICS_BASED_ALERT:
|
||||
default:
|
||||
return (
|
||||
<Tabs
|
||||
type="card"
|
||||
style={{ width: '100%' }}
|
||||
defaultActiveKey={EQueryType.QUERY_BUILDER}
|
||||
activeKey={queryCategory}
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<Button type="primary" onClick={runQuery}>
|
||||
Run Query
|
||||
</Button>
|
||||
</span>
|
||||
}
|
||||
items={items}
|
||||
/>
|
||||
<div className="alert-tabs">
|
||||
<Tabs
|
||||
type="card"
|
||||
style={{ width: '100%' }}
|
||||
defaultActiveKey={currentTab}
|
||||
activeKey={currentTab}
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span style={{ display: 'flex', gap: '1rem', alignItems: 'center' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={runQuery}
|
||||
className="stage-run-query"
|
||||
icon={<Play size={14} />}
|
||||
>
|
||||
Stage & Run Query
|
||||
</Button>
|
||||
</span>
|
||||
}
|
||||
items={items}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -126,8 +184,8 @@ function QuerySection({
|
||||
<>
|
||||
<StepHeading> {t('alert_form_step1')}</StepHeading>
|
||||
<FormContainer>
|
||||
<div style={{ display: 'flex' }}>{renderTabs(alertType)}</div>
|
||||
{renderQuerySection(queryCategory)}
|
||||
<div>{renderTabs(alertType)}</div>
|
||||
{renderQuerySection(currentTab)}
|
||||
</FormContainer>
|
||||
</>
|
||||
);
|
||||
|
@ -83,7 +83,7 @@ function RuleOptions({
|
||||
</InlineSelect>
|
||||
);
|
||||
|
||||
const renderThresholdMatchOpts = (): JSX.Element => (
|
||||
const renderMatchOpts = (): JSX.Element => (
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
defaultValue={defaultMatchType}
|
||||
@ -98,17 +98,13 @@ function RuleOptions({
|
||||
</InlineSelect>
|
||||
);
|
||||
|
||||
const renderPromMatchOpts = (): JSX.Element => (
|
||||
<InlineSelect
|
||||
getPopupContainer={popupContainer}
|
||||
defaultValue={defaultMatchType}
|
||||
style={{ minWidth: '130px' }}
|
||||
value={alertDef.condition?.matchType}
|
||||
onChange={(value: string | unknown): void => handleMatchOptChange(value)}
|
||||
>
|
||||
<Select.Option value="1">{t('option_atleastonce')}</Select.Option>
|
||||
</InlineSelect>
|
||||
);
|
||||
const onChangeEvalWindow = (value: string | unknown): void => {
|
||||
const ew = (value as string) || alertDef.evalWindow;
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
evalWindow: ew,
|
||||
});
|
||||
};
|
||||
|
||||
const renderEvalWindows = (): JSX.Element => (
|
||||
<InlineSelect
|
||||
@ -116,13 +112,7 @@ function RuleOptions({
|
||||
defaultValue={defaultEvalWindow}
|
||||
style={{ minWidth: '120px' }}
|
||||
value={alertDef.evalWindow}
|
||||
onChange={(value: string | unknown): void => {
|
||||
const ew = (value as string) || alertDef.evalWindow;
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
evalWindow: ew,
|
||||
});
|
||||
}}
|
||||
onChange={onChangeEvalWindow}
|
||||
>
|
||||
<Select.Option value="5m0s">{t('option_5min')}</Select.Option>
|
||||
<Select.Option value="10m0s">{t('option_10min')}</Select.Option>
|
||||
@ -133,6 +123,20 @@ function RuleOptions({
|
||||
</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 => (
|
||||
<Form.Item>
|
||||
<Typography.Text>
|
||||
@ -147,7 +151,7 @@ function RuleOptions({
|
||||
onChange={onChangeSelectedQueryName}
|
||||
/>
|
||||
<Typography.Text>is</Typography.Text>
|
||||
{renderCompareOps()} {t('text_condition2')} {renderThresholdMatchOpts()}{' '}
|
||||
{renderCompareOps()} {t('text_condition2')} {renderMatchOpts()}{' '}
|
||||
{t('text_condition3')} {renderEvalWindows()}
|
||||
</Typography.Text>
|
||||
</Form.Item>
|
||||
@ -167,7 +171,8 @@ function RuleOptions({
|
||||
onChange={onChangeSelectedQueryName}
|
||||
/>
|
||||
<Typography.Text>is</Typography.Text>
|
||||
{renderCompareOps()} {t('text_condition2')} {renderPromMatchOpts()}
|
||||
{renderCompareOps()} {t('text_condition2')} {renderMatchOpts()}
|
||||
{t('text_condition3')} {renderPromEvalWindows()}
|
||||
</Typography.Text>
|
||||
</Form.Item>
|
||||
);
|
||||
|
@ -142,6 +142,10 @@ function FormAlertRules({
|
||||
// onQueryCategoryChange handles changes to query category
|
||||
// in state as well as sets additional defaults
|
||||
const onQueryCategoryChange = (val: EQueryType): void => {
|
||||
const element = document.getElementById('top');
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
if (val === EQueryType.PROM) {
|
||||
setAlertDef({
|
||||
...alertDef,
|
||||
@ -465,7 +469,7 @@ function FormAlertRules({
|
||||
<>
|
||||
{Element}
|
||||
|
||||
<PanelContainer>
|
||||
<PanelContainer id="top">
|
||||
<StyledLeftContainer flex="5 1 600px" md={18}>
|
||||
<MainFormContainer
|
||||
initialValues={initialValue}
|
||||
|
@ -76,6 +76,10 @@ export const FormContainer = styled(Card)`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 4px;
|
||||
|
||||
.ant-card-body {
|
||||
padding: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
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 setRetentionApi from 'api/settings/setRetention';
|
||||
import TextToolTip from 'components/TextToolTip';
|
||||
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import find from 'lodash-es/find';
|
||||
@ -24,6 +25,7 @@ import {
|
||||
PayloadPropsTraces as GetRetentionPeriodTracesPayload,
|
||||
} from 'types/api/settings/getRetention';
|
||||
import AppReducer from 'types/reducer/app';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
import Retention from './Retention';
|
||||
import StatusMessage from './StatusMessage';
|
||||
@ -394,6 +396,8 @@ function GeneralSettings({
|
||||
onModalToggleHandler(type);
|
||||
};
|
||||
|
||||
const isCloudUserVal = isCloudUser();
|
||||
|
||||
const renderConfig = [
|
||||
{
|
||||
name: 'Metrics',
|
||||
@ -521,7 +525,7 @@ function GeneralSettings({
|
||||
return (
|
||||
<Fragment key={category.name}>
|
||||
<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}>
|
||||
{category.name}
|
||||
</Typography.Title>
|
||||
@ -542,38 +546,43 @@ function GeneralSettings({
|
||||
hide={!!retentionField.hide}
|
||||
/>
|
||||
))}
|
||||
<ActionItemsContainer>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={category.save.modalOpen}
|
||||
disabled={category.save.isDisabled}
|
||||
>
|
||||
{category.save.saveButtonText}
|
||||
</Button>
|
||||
{category.statusComponent}
|
||||
</ActionItemsContainer>
|
||||
<Modal
|
||||
title={t('retention_confirmation')}
|
||||
focusTriggerAfterClose
|
||||
forceRender
|
||||
destroyOnClose
|
||||
closable
|
||||
onCancel={(): void =>
|
||||
onModalToggleHandler(category.name.toLowerCase() as TTTLType)
|
||||
}
|
||||
onOk={(): Promise<void> =>
|
||||
onOkHandler(category.name.toLowerCase() as TTTLType)
|
||||
}
|
||||
centered
|
||||
open={category.save.modal}
|
||||
confirmLoading={category.save.apiLoading}
|
||||
>
|
||||
<Typography>
|
||||
{t('retention_confirmation_description', {
|
||||
name: category.name.toLowerCase(),
|
||||
})}
|
||||
</Typography>
|
||||
</Modal>
|
||||
|
||||
{!isCloudUserVal && (
|
||||
<>
|
||||
<ActionItemsContainer>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={category.save.modalOpen}
|
||||
disabled={category.save.isDisabled}
|
||||
>
|
||||
{category.save.saveButtonText}
|
||||
</Button>
|
||||
{category.statusComponent}
|
||||
</ActionItemsContainer>
|
||||
<Modal
|
||||
title={t('retention_confirmation')}
|
||||
focusTriggerAfterClose
|
||||
forceRender
|
||||
destroyOnClose
|
||||
closable
|
||||
onCancel={(): void =>
|
||||
onModalToggleHandler(category.name.toLowerCase() as TTTLType)
|
||||
}
|
||||
onOk={(): Promise<void> =>
|
||||
onOkHandler(category.name.toLowerCase() as TTTLType)
|
||||
}
|
||||
centered
|
||||
open={category.save.modal}
|
||||
confirmLoading={category.save.apiLoading}
|
||||
>
|
||||
<Typography>
|
||||
{t('retention_confirmation_description', {
|
||||
name: category.name.toLowerCase(),
|
||||
})}
|
||||
</Typography>
|
||||
</Modal>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
</Fragment>
|
||||
@ -587,16 +596,20 @@ function GeneralSettings({
|
||||
{Element}
|
||||
<Col xs={24} md={22} xl={20} xxl={18} style={{ margin: 'auto' }}>
|
||||
<ErrorTextContainer>
|
||||
<TextToolTip
|
||||
{...{
|
||||
text: `More details on how to set retention period`,
|
||||
url: 'https://signoz.io/docs/userguide/retention-period/',
|
||||
}}
|
||||
/>
|
||||
{!isCloudUserVal && (
|
||||
<TextToolTip
|
||||
{...{
|
||||
text: `More details on how to set retention period`,
|
||||
url: 'https://signoz.io/docs/userguide/retention-period/',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{errorText && <ErrorText>{errorText}</ErrorText>}
|
||||
</ErrorTextContainer>
|
||||
|
||||
<Row justify="start">{renderConfig}</Row>
|
||||
|
||||
{isCloudUserVal && <GeneralSettingsCloud />}
|
||||
</Col>
|
||||
</>
|
||||
);
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { isCloudUser } from 'utils/app';
|
||||
|
||||
import {
|
||||
Input,
|
||||
@ -85,9 +86,13 @@ function Retention({
|
||||
func(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (hide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isCloudUserVal = isCloudUser();
|
||||
|
||||
return (
|
||||
<RetentionContainer>
|
||||
<Row justify="space-between">
|
||||
@ -98,12 +103,14 @@ function Retention({
|
||||
<RetentionFieldInputContainer>
|
||||
<Input
|
||||
value={selectedValue && selectedValue >= 0 ? selectedValue : ''}
|
||||
disabled={isCloudUserVal}
|
||||
onChange={(e): void => onChangeHandler(e, setSelectedValue)}
|
||||
style={{ width: 75 }}
|
||||
/>
|
||||
<Select
|
||||
value={selectedTimeUnit}
|
||||
onChange={currentSelectedOption}
|
||||
disabled={isCloudUserVal}
|
||||
style={{ width: 100 }}
|
||||
>
|
||||
{menuItems}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { green, orange, volcano } from '@ant-design/colors';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Card, Col, Row } from 'antd';
|
||||
import { Card, Col } from 'antd';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TStatus } from 'types/api/settings/getRetention';
|
||||
@ -43,9 +43,16 @@ function StatusMessage({
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Row style={{ gap: '1rem', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '1rem',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Col xs={1}>
|
||||
<InfoCircleOutlined style={{ fontSize: '1.5rem' }} />
|
||||
<InfoCircleOutlined style={{ fontSize: '1rem' }} />
|
||||
</Col>
|
||||
|
||||
<Col
|
||||
@ -56,7 +63,7 @@ function StatusMessage({
|
||||
>
|
||||
{statusMessage}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</Card>
|
||||
) : null;
|
||||
}
|
||||
|
@ -1,7 +1,11 @@
|
||||
.general-settings-container {
|
||||
margin: 16px 8px;
|
||||
|
||||
.ant-card-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 8px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ function HeaderContainer(): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { toggleTheme } = useThemeMode();
|
||||
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);
|
||||
const [homeRoute, setHomeRoute] = useState(ROUTES.APPLICATION);
|
||||
const [homeRoute, setHomeRoute] = useState<string>(ROUTES.APPLICATION);
|
||||
|
||||
const [isUserDropDownOpen, setIsUserDropDownOpen] = useState<boolean>(false);
|
||||
|
||||
|
@ -84,6 +84,7 @@ export default function IngestionSettings(): JSX.Element {
|
||||
pagination={false}
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Card, Typography } from 'antd';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import { VIEW_TYPES } from 'components/LogDetail/constants';
|
||||
import ListLogView from 'components/Logs/ListLogView';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import Spinner from 'components/Spinner';
|
||||
@ -13,7 +14,6 @@ import { Heading } from 'container/LogsTable/styles';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { useActiveLog } from 'hooks/logs/useActiveLog';
|
||||
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
|
||||
import useFontFaceObserver from 'hooks/useFontObserver';
|
||||
import { useEventSource } from 'providers/EventSource';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -51,19 +51,6 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
[logs, activeLogId],
|
||||
);
|
||||
|
||||
useFontFaceObserver(
|
||||
[
|
||||
{
|
||||
family: 'Fira Code',
|
||||
weight: '300',
|
||||
},
|
||||
],
|
||||
options.format === 'raw',
|
||||
{
|
||||
timeout: 5000,
|
||||
},
|
||||
);
|
||||
|
||||
const selectedFields = convertKeysToColumnFields(options.selectColumns);
|
||||
|
||||
const getItemContent = useCallback(
|
||||
@ -150,6 +137,7 @@ function LiveLogsList({ logs }: LiveLogsListProps): JSX.Element {
|
||||
</InfinityWrapperStyled>
|
||||
)}
|
||||
<LogDetail
|
||||
selectedTab={VIEW_TYPES.OVERVIEW}
|
||||
log={activeLog}
|
||||
onClose={onClearActiveLog}
|
||||
onAddToQuery={onAddToQuery}
|
||||
|
@ -29,11 +29,11 @@ function ActionItem({
|
||||
() => (
|
||||
<Col>
|
||||
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.IN)}>
|
||||
<PlusCircleOutlined /> Filter for value
|
||||
<PlusCircleOutlined size={12} /> Filter for value
|
||||
</Button>
|
||||
<br />
|
||||
<Button type="text" size="small" onClick={onClickHandler(OPERATORS.NIN)}>
|
||||
<MinusCircleOutlined /> Filter out value
|
||||
<MinusCircleOutlined size={12} /> Filter out value
|
||||
</Button>
|
||||
</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;
|