Compare commits

...

36 Commits

Author SHA1 Message Date
Karan Balani
c43df52878 fix: open api specs and renamed imports in frontend 2026-03-16 22:38:51 +05:30
Karan Balani
6277bd47b4 refactor: deduplicated err code for password not found 2026-03-16 22:16:34 +05:30
Karan Balani
5095573c51 refactor: fix imports 2026-03-16 22:08:49 +05:30
Karan Balani
aeba33646d refactor: merge roletypes into authtypes and move role.go to authtypes as legacy_role.go 2026-03-16 21:53:21 +05:30
Karan Balani
4f2ce5ecb9 refactor: add usertypes package and move user related files there for consistency 2026-03-16 20:05:56 +05:30
Pandey
cab4a56694 chore: add myself as codeowner for CI and go.mod (#10597)
Clarified CODEOWNERS comments and updated owner assignments.
2026-03-16 10:01:36 +00:00
Ashwin Bhatkal
78041fe457 chore: send slack notification on dequeue only and not merge (#10596)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
2026-03-16 09:38:04 +00:00
Ashwin Bhatkal
09b6382820 chore: separate dashboard slider from dashboard provider + refactor (#10572)
* chore: separate dashboard slider from dashboard provider + refactor

* chore: resolve self comments
2026-03-16 08:12:09 +00:00
Ashwin Bhatkal
9689b847f0 chore: add slack notification on dequeue from merge queue (#10580)
* chore: add slack notification on merge queue failure

* chore: break type

* chore: update yaml

* chore: update yaml

* chore: update yaml

* chore: update yaml

* chore: update yaml

* chore: update yaml

* chore: update yaml

* chore: resolve comments
2026-03-16 07:12:19 +00:00
Vishal Sharma
15e5938e95 fix: add allInOneLightMode SVG for light mode (#10589) 2026-03-16 06:59:28 +00:00
Abhi kumar
c5ef455283 fix: added fix for panel setting scrollbar issue (#10587)
Some checks failed
build-staging / prepare (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
* fix: added fix for panel setting scrollbar issue

* fix: added changes for panel switch
2026-03-13 19:30:49 +00:00
Ishan
2316b5be83 Sig 3634 revert (#10578)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* Revert "Revert "feat: Option to zoom out OR reset zoom in the explorer pages (#10464)" (#10574)"

This reverts commit 5b8d5fbfd3.

* fix: stop bubble
2026-03-13 15:29:28 +00:00
Abhi kumar
937ebc1582 feat: added section in panel settings (#10569)
* feat: added section in panel settings

* chore: minor changes

* fix: fixed failing tests

* fix: minor style fixes

* chore: updated the categorisation

* chore: updated styles

* chore: minor styles improvements

* chore: formatting unit section fix
2026-03-13 13:22:10 +00:00
Ashwin Bhatkal
dcc8173c79 fix: variables initial url state (#10579)
* fix: variables-initial-url-state

* chore: add tests
2026-03-13 11:16:47 +00:00
Ashwin Bhatkal
4b4ef5ce58 fix: edit mode variables not persisting value (#10576)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix: edit mode variables not persisting value

* chore: move into hook

* chore: add tests

* chore: fix tests

* chore: move functions
2026-03-13 07:49:40 +00:00
Yunus M
5b8d5fbfd3 Revert "feat: Option to zoom out OR reset zoom in the explorer pages (#10464)" (#10574)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
This reverts commit 557451ed81.
2026-03-12 19:24:49 +00:00
Ashwin Bhatkal
0271be11e6 chore: remove dashboard provider from the root (#10526)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: remove dashboard provider from the root

* chore: fix tests

* chore: fix tests

* chore: remove dashboardId from provider

* chore: remove old instances of dashboard provider

* chore: separate dashboard widget fully

* chore: fix tests

* chore: resolve self comments
2026-03-12 14:51:49 +00:00
Vikrant Gupta
92d220c4d9 feat(serviceaccount): domain changes for service account (#10568)
* feat(serviceaccount): domain type changes

* feat(serviceaccount): domain type changes

* feat(serviceaccount): domain type changes
2026-03-12 11:06:04 +00:00
Vikrant Gupta
0ed8169bad feat(authz): add service account authz changes (#10567) 2026-03-12 09:42:50 +00:00
SagarRajput-7
ed553fb02e feat: removed plan name and added copiable license info in custom domain card (#10558)
* feat: removed plan name and added copiable license info in custom domain card

* feat: added condition on the license row in custom domain card

* feat: code refactor and making license row a common component

* feat: added test case and addressed feedback

* feat: style improvement

* feat: added maskedkey util and refactored code

* feat: updated test case
2026-03-12 09:24:41 +00:00
Ashwin Bhatkal
47daba3c17 chore: link session url with sentry alert (#10566) 2026-03-12 09:19:31 +00:00
Srikanth Chekuri
2b3310809a fix: newServer uses the stored config hash for mismatch (#10563) 2026-03-12 08:26:22 +00:00
Ashwin Bhatkal
542a648cc3 chore: remove toScrollWidgetId from dashboard provider (#10562)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* chore: remove toScrollWidgetId from dashboard provider

* chore: remove dead files

* chore: fix tests
2026-03-12 06:03:02 +00:00
Naman Verma
61df12d126 test: integration tests for percentile aggregation (#10555)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix: make histogramQuantile function work for devenv setup

* fix: make histogramQuantile function work for integration tests

* test: histogram percentile integration tests

* chore: explicitly mention user_scripts_path

* chore: fail tests if download of UDF fails
2026-03-11 14:27:01 +00:00
Vinicius Lourenço
b846faa1fa fix(app-routes): do not render old route, redirect instead (#10553) 2026-03-11 12:53:32 +00:00
Ishan
557451ed81 feat: Option to zoom out OR reset zoom in the explorer pages (#10464)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* feat: zoom out func ladder added

* feat: zoom out feature with testcases

* fix: comments resolved moved to signoz btn added testcase for querydelete

* feat: updated btn compoent to use prefix icon

* feat: historical enddate preset as null to preserve custom

* fix: cursor bot callback and localstorage

* feat: common util for local storage

* feat: rename and testcase

* feat: avoid persist for non preset
2026-03-11 07:23:30 +00:00
Ishan
25c513ec2f fix: updated fallback color (#10525)
* fix: updated fallback color

* fix: updated testcase
2026-03-11 07:23:21 +00:00
primus-bot[bot]
ae71f2608a chore(release): bump to v0.115.0 (#10556)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2026-03-11 07:11:28 +00:00
Ishan
f7140832d0 Fix - Handling for resource. prefix in quick filters (#10497)
* fix: resource added in checkbox

* fix: resource name handling

* fix: added testcases

* fix: updated for other types as well

* fix: updated testcases

* fix: pr comments
2026-03-11 06:31:53 +00:00
Yunus M
8c5ff10b42 feat: update url with y-axis unit (#10530)
* feat: update url with y-axis unit

* feat: implement useUrlYAxisUnit hook for y-axis unit management

* feat: use nuqs for handling query param updates

* chore: whitelist nuqs in jest

* chore: add nuqs to test utils
2026-03-11 05:10:47 +00:00
Vishal Sharma
12c0df8410 feat(onboarding): add configs and SVGs for 9 new datasources (#10552)
* chore: add new integration logos and update onboarding configurations and related files

* fix(logos): add viewBox attribute to huggingface svg for proper scaling
2026-03-11 04:58:18 +00:00
Ashwin Bhatkal
a139915f4e fix: guard against undefined spread in useGetQueryLabels (#10550)
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix: guard against undefined spread in useGetQueryLabels

* chore: add tests
2026-03-10 18:30:31 +00:00
Abhi kumar
b69bcd63ba fix: added fix for apiresponse being undefined in panel config creation (#10549)
* fix: added fix for apiresponse being undefined in panel config creation

* chore: pr review changes
2026-03-10 17:35:34 +00:00
Ashwin Bhatkal
f576a86dd1 fix: avoid read-only variables mutation (#10548) 2026-03-10 17:22:47 +00:00
Ashwin Bhatkal
996c9a891f chore: remove selectedRowWidgetId from provider (#10547)
* chore: remove selectedRowWidgetId from provider

* chore: rename helper

* chore: add comment
2026-03-10 16:57:38 +00:00
Ashwin Bhatkal
d1a872dadc chore: remove dashboardId from provider (#10546) 2026-03-10 16:42:25 +00:00
219 changed files with 7378 additions and 3259 deletions

View File

@@ -1,4 +1,22 @@
services:
init-clickhouse:
image: clickhouse/clickhouse-server:25.5.6
container_name: init-clickhouse
command:
- bash
- -c
- |
version="v0.0.1"
node_os=$$(uname -s | tr '[:upper:]' '[:lower:]')
node_arch=$$(uname -m | sed s/aarch64/arm64/ | sed s/x86_64/amd64/)
echo "Fetching histogram-binary for $${node_os}/$${node_arch}"
cd /tmp
wget -O histogram-quantile.tar.gz "https://github.com/SigNoz/signoz/releases/download/histogram-quantile%2F$${version}/histogram-quantile_$${node_os}_$${node_arch}.tar.gz"
tar -xvzf histogram-quantile.tar.gz
mv histogram-quantile /var/lib/clickhouse/user_scripts/histogramQuantile
restart: on-failure
volumes:
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
clickhouse:
image: clickhouse/clickhouse-server:25.5.6
container_name: clickhouse
@@ -7,6 +25,7 @@ services:
- ${PWD}/fs/etc/clickhouse-server/users.d/users.xml:/etc/clickhouse-server/users.d/users.xml
- ${PWD}/fs/tmp/var/lib/clickhouse/:/var/lib/clickhouse/
- ${PWD}/fs/tmp/var/lib/clickhouse/user_scripts/:/var/lib/clickhouse/user_scripts/
- ${PWD}/../../../deploy/common/clickhouse/custom-function.xml:/etc/clickhouse-server/custom-function.xml
ports:
- '127.0.0.1:8123:8123'
- '127.0.0.1:9000:9000'
@@ -22,7 +41,10 @@ services:
timeout: 5s
retries: 3
depends_on:
- zookeeper
init-clickhouse:
condition: service_completed_successfully
zookeeper:
condition: service_healthy
environment:
- CLICKHOUSE_SKIP_USER_SETUP=1
zookeeper:

View File

@@ -44,4 +44,6 @@
<shard>01</shard>
<replica>01</replica>
</macros>
<user_defined_executable_functions_config>*function.xml</user_defined_executable_functions_config>
<user_scripts_path>/var/lib/clickhouse/user_scripts/</user_scripts_path>
</clickhouse>

15
.github/CODEOWNERS vendored
View File

@@ -1,8 +1,6 @@
# CODEOWNERS info: https://help.github.com/en/articles/about-code-owners
# Owners are automatically requested for review for PRs that changes code
# that they own.
# Owners are automatically requested for review for PRs that changes code that they own.
/frontend/ @SigNoz/frontend-maintainers
@@ -11,8 +9,10 @@
/frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json @makeavish
/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @makeavish
/deploy/ @SigNoz/devops
.github @SigNoz/devops
# CI
/deploy/ @therealpandey
.github @therealpandey
go.mod @therealpandey
# Scaffold Owners
@@ -127,12 +127,15 @@
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
# Dashboard Widget Page
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
## Dashboard Page
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
## Public Dashboard Page

60
.github/workflows/mergequeueci.yaml vendored Normal file
View File

@@ -0,0 +1,60 @@
name: mergequeueci
on:
pull_request:
types:
- dequeued
jobs:
notify:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == false
steps:
- name: alert
uses: slackapi/slack-github-action@v2.1.1
with:
webhook: ${{ secrets.SLACK_MERGE_QUEUE_WEBHOOK }}
webhook-type: incoming-webhook
payload: |
{
"text": ":x: PR removed from merge queue",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": ":x: PR Removed from Merge Queue"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*<${{ github.event.pull_request.html_url }}|PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}>*"
}
},
{
"type": "divider"
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": "*Author*\n@${{ github.event.pull_request.user.login }}"
}
]
}
]
}
- name: comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_URL: ${{ github.event.pull_request.html_url }}
run: |
gh api repos/${{ github.repository }}/issues/$PR_NUMBER/comments \
-f body="> :x: **PR removed from merge queue**
>
> @$PR_AUTHOR your PR was removed from the merge queue. Fix the issue and re-queue when ready."

View File

@@ -18,6 +18,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
"github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/query-service/app"
"github.com/SigNoz/signoz/pkg/queryparser"
@@ -73,8 +74,8 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
},
signoz.NewSQLStoreProviderFactories(),
signoz.NewTelemetryStoreProviderFactories(),
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
return signoz.NewAuthNs(ctx, providerSettings, store, licensing)
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing, userGetter user.Getter) (map[authtypes.AuthNProvider]authn.AuthN, error) {
return signoz.NewAuthNs(ctx, providerSettings, store, licensing, userGetter)
},
func(ctx context.Context, sqlstore sqlstore.SQLStore, _ licensing.Licensing, _ dashboard.Module) factory.ProviderFactory[authz.AuthZ, authz.Config] {
return openfgaauthz.NewProviderFactory(sqlstore, openfgaschema.NewSchema().Get(ctx))

View File

@@ -9,12 +9,12 @@ import (
"github.com/SigNoz/signoz/ee/authn/callbackauthn/oidccallbackauthn"
"github.com/SigNoz/signoz/ee/authn/callbackauthn/samlcallbackauthn"
"github.com/SigNoz/signoz/ee/authz/openfgaauthz"
eequerier "github.com/SigNoz/signoz/ee/querier"
"github.com/SigNoz/signoz/ee/authz/openfgaschema"
"github.com/SigNoz/signoz/ee/gateway/httpgateway"
enterpriselicensing "github.com/SigNoz/signoz/ee/licensing"
"github.com/SigNoz/signoz/ee/licensing/httplicensing"
"github.com/SigNoz/signoz/ee/modules/dashboard/impldashboard"
eequerier "github.com/SigNoz/signoz/ee/querier"
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
"github.com/SigNoz/signoz/ee/sqlschema/postgressqlschema"
"github.com/SigNoz/signoz/ee/sqlstore/postgressqlstore"
@@ -29,6 +29,7 @@ import (
"github.com/SigNoz/signoz/pkg/modules/dashboard"
pkgimpldashboard "github.com/SigNoz/signoz/pkg/modules/dashboard/impldashboard"
"github.com/SigNoz/signoz/pkg/modules/organization"
"github.com/SigNoz/signoz/pkg/modules/user"
"github.com/SigNoz/signoz/pkg/querier"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/signoz"
@@ -95,7 +96,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
},
sqlstoreFactories,
signoz.NewTelemetryStoreProviderFactories(),
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing) (map[authtypes.AuthNProvider]authn.AuthN, error) {
func(ctx context.Context, providerSettings factory.ProviderSettings, store authtypes.AuthNStore, licensing licensing.Licensing, userGetter user.Getter) (map[authtypes.AuthNProvider]authn.AuthN, error) {
samlCallbackAuthN, err := samlcallbackauthn.New(ctx, store, licensing)
if err != nil {
return nil, err
@@ -106,7 +107,7 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
return nil, err
}
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing)
authNs, err := signoz.NewAuthNs(ctx, providerSettings, store, licensing, userGetter)
if err != nil {
return nil, err
}

View File

@@ -190,7 +190,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.114.1
image: signoz/signoz:v0.115.0
ports:
- "8080:8080" # signoz port
# - "6060:6060" # pprof port

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:v0.114.1
image: signoz/signoz:v0.115.0
ports:
- "8080:8080" # signoz port
volumes:

View File

@@ -181,7 +181,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.114.1}
image: signoz/signoz:${VERSION:-v0.115.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -109,7 +109,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz:
!!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.114.1}
image: signoz/signoz:${VERSION:-v0.115.0}
container_name: signoz
ports:
- "8080:8080" # signoz port

View File

@@ -220,6 +220,13 @@ components:
- additions
- deletions
type: object
AuthtypesPatchableRole:
properties:
description:
type: string
required:
- description
type: object
AuthtypesPostableAuthDomain:
properties:
config:
@@ -236,6 +243,15 @@ components:
password:
type: string
type: object
AuthtypesPostableRole:
properties:
description:
type: string
name:
type: string
required:
- name
type: object
AuthtypesPostableRotateToken:
properties:
refreshToken:
@@ -251,6 +267,31 @@ components:
- name
- type
type: object
AuthtypesRole:
properties:
createdAt:
format: date-time
type: string
description:
type: string
id:
type: string
name:
type: string
orgId:
type: string
type:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- description
- type
- orgId
type: object
AuthtypesRoleMapping:
properties:
defaultRole:
@@ -1722,65 +1763,24 @@ components:
- status
- error
type: object
RoletypesPatchableRole:
properties:
description:
type: string
required:
- description
type: object
RoletypesPostableRole:
properties:
description:
type: string
name:
type: string
required:
- name
type: object
RoletypesRole:
properties:
createdAt:
format: date-time
type: string
description:
type: string
id:
type: string
name:
type: string
orgId:
type: string
type:
type: string
updatedAt:
format: date-time
type: string
required:
- id
- name
- description
- type
- orgId
type: object
ServiceaccounttypesFactorAPIKey:
properties:
createdAt:
format: date-time
type: string
expires_at:
expiresAt:
minimum: 0
type: integer
id:
type: string
key:
type: string
last_used:
lastObservedAt:
format: date-time
type: string
name:
type: string
service_account_id:
serviceAccountId:
type: string
updatedAt:
format: date-time
@@ -1788,9 +1788,9 @@ components:
required:
- id
- key
- expires_at
- last_used
- service_account_id
- expiresAt
- lastObservedAt
- serviceAccountId
type: object
ServiceaccounttypesGettableFactorAPIKeyWithKey:
properties:
@@ -1804,14 +1804,14 @@ components:
type: object
ServiceaccounttypesPostableFactorAPIKey:
properties:
expires_at:
expiresAt:
minimum: 0
type: integer
name:
type: string
required:
- name
- expires_at
- expiresAt
type: object
ServiceaccounttypesPostableServiceAccount:
properties:
@@ -1833,13 +1833,16 @@ components:
createdAt:
format: date-time
type: string
deletedAt:
format: date-time
type: string
email:
type: string
id:
type: string
name:
type: string
orgID:
orgId:
type: string
roles:
items:
@@ -1856,18 +1859,19 @@ components:
- email
- roles
- status
- orgID
- orgId
- deletedAt
type: object
ServiceaccounttypesUpdatableFactorAPIKey:
properties:
expires_at:
expiresAt:
minimum: 0
type: integer
name:
type: string
required:
- name
- expires_at
- expiresAt
type: object
ServiceaccounttypesUpdatableServiceAccount:
properties:
@@ -1980,52 +1984,6 @@ components:
type: string
type: array
type: object
TypesChangePasswordRequest:
properties:
newPassword:
type: string
oldPassword:
type: string
userId:
type: string
type: object
TypesGettableAPIKey:
properties:
createdAt:
format: date-time
type: string
createdBy:
type: string
createdByUser:
$ref: '#/components/schemas/TypesUser'
expiresAt:
format: int64
type: integer
id:
type: string
lastUsed:
format: int64
type: integer
name:
type: string
revoked:
type: boolean
role:
type: string
token:
type: string
updatedAt:
format: date-time
type: string
updatedBy:
type: string
updatedByUser:
$ref: '#/components/schemas/TypesUser'
userId:
type: string
required:
- id
type: object
TypesGettableGlobalConfig:
properties:
external_url:
@@ -2040,31 +1998,6 @@ components:
required:
- id
type: object
TypesInvite:
properties:
createdAt:
format: date-time
type: string
email:
type: string
id:
type: string
inviteLink:
type: string
name:
type: string
orgId:
type: string
role:
type: string
token:
type: string
updatedAt:
format: date-time
type: string
required:
- id
type: object
TypesOrganization:
properties:
alias:
@@ -2087,7 +2020,78 @@ components:
required:
- id
type: object
TypesPostableAPIKey:
UsertypesChangePasswordRequest:
properties:
newPassword:
type: string
oldPassword:
type: string
userId:
type: string
type: object
UsertypesGettableAPIKey:
properties:
createdAt:
format: date-time
type: string
createdBy:
type: string
createdByUser:
$ref: '#/components/schemas/UsertypesUser'
expiresAt:
format: int64
type: integer
id:
type: string
lastUsed:
format: int64
type: integer
name:
type: string
revoked:
type: boolean
role:
type: string
token:
type: string
updatedAt:
format: date-time
type: string
updatedBy:
type: string
updatedByUser:
$ref: '#/components/schemas/UsertypesUser'
userId:
type: string
required:
- id
type: object
UsertypesInvite:
properties:
createdAt:
format: date-time
type: string
email:
type: string
id:
type: string
inviteLink:
type: string
name:
type: string
orgId:
type: string
role:
type: string
token:
type: string
updatedAt:
format: date-time
type: string
required:
- id
type: object
UsertypesPostableAPIKey:
properties:
expiresInDays:
format: int64
@@ -2097,7 +2101,7 @@ components:
role:
type: string
type: object
TypesPostableAcceptInvite:
UsertypesPostableAcceptInvite:
properties:
displayName:
type: string
@@ -2108,16 +2112,16 @@ components:
token:
type: string
type: object
TypesPostableBulkInviteRequest:
UsertypesPostableBulkInviteRequest:
properties:
invites:
items:
$ref: '#/components/schemas/TypesPostableInvite'
$ref: '#/components/schemas/UsertypesPostableInvite'
type: array
required:
- invites
type: object
TypesPostableForgotPassword:
UsertypesPostableForgotPassword:
properties:
email:
type: string
@@ -2129,7 +2133,7 @@ components:
- orgId
- email
type: object
TypesPostableInvite:
UsertypesPostableInvite:
properties:
email:
type: string
@@ -2140,14 +2144,14 @@ components:
role:
type: string
type: object
TypesPostableResetPassword:
UsertypesPostableResetPassword:
properties:
password:
type: string
token:
type: string
type: object
TypesResetPasswordToken:
UsertypesResetPasswordToken:
properties:
expiresAt:
format: date-time
@@ -2161,7 +2165,7 @@ components:
required:
- id
type: object
TypesStorableAPIKey:
UsertypesStorableAPIKey:
properties:
createdAt:
format: date-time
@@ -2188,7 +2192,7 @@ components:
required:
- id
type: object
TypesUser:
UsertypesUser:
properties:
createdAt:
format: date-time
@@ -2388,7 +2392,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesChangePasswordRequest'
$ref: '#/components/schemas/UsertypesChangePasswordRequest'
responses:
"204":
description: No Content
@@ -3193,7 +3197,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/TypesResetPasswordToken'
$ref: '#/components/schemas/UsertypesResetPasswordToken'
status:
type: string
required:
@@ -3298,7 +3302,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/TypesInvite'
$ref: '#/components/schemas/UsertypesInvite'
type: array
status:
type: string
@@ -3341,7 +3345,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableInvite'
$ref: '#/components/schemas/UsertypesPostableInvite'
responses:
"201":
content:
@@ -3349,7 +3353,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/TypesInvite'
$ref: '#/components/schemas/UsertypesInvite'
status:
type: string
required:
@@ -3465,7 +3469,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/TypesInvite'
$ref: '#/components/schemas/UsertypesInvite'
status:
type: string
required:
@@ -3503,7 +3507,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableAcceptInvite'
$ref: '#/components/schemas/UsertypesPostableAcceptInvite'
responses:
"201":
content:
@@ -3511,7 +3515,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/TypesUser'
$ref: '#/components/schemas/UsertypesUser'
status:
type: string
required:
@@ -3549,7 +3553,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableBulkInviteRequest'
$ref: '#/components/schemas/UsertypesPostableBulkInviteRequest'
responses:
"201":
description: Created
@@ -3874,7 +3878,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/TypesGettableAPIKey'
$ref: '#/components/schemas/UsertypesGettableAPIKey'
type: array
status:
type: string
@@ -3917,7 +3921,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableAPIKey'
$ref: '#/components/schemas/UsertypesPostableAPIKey'
responses:
"201":
content:
@@ -3925,7 +3929,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/TypesGettableAPIKey'
$ref: '#/components/schemas/UsertypesGettableAPIKey'
status:
type: string
required:
@@ -4031,7 +4035,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesStorableAPIKey'
$ref: '#/components/schemas/UsertypesStorableAPIKey'
responses:
"204":
content:
@@ -4192,7 +4196,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableResetPassword'
$ref: '#/components/schemas/UsertypesPostableResetPassword'
responses:
"204":
description: No Content
@@ -4230,7 +4234,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/RoletypesRole'
$ref: '#/components/schemas/AuthtypesRole'
type: array
status:
type: string
@@ -4273,7 +4277,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/RoletypesPostableRole'
$ref: '#/components/schemas/AuthtypesPostableRole'
responses:
"201":
content:
@@ -4418,7 +4422,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/RoletypesRole'
$ref: '#/components/schemas/AuthtypesRole'
status:
type: string
required:
@@ -4466,7 +4470,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/RoletypesPatchableRole'
$ref: '#/components/schemas/AuthtypesPatchableRole'
responses:
"204":
content:
@@ -5267,7 +5271,7 @@ paths:
properties:
data:
items:
$ref: '#/components/schemas/TypesUser'
$ref: '#/components/schemas/UsertypesUser'
type: array
status:
type: string
@@ -5365,7 +5369,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/TypesUser'
$ref: '#/components/schemas/UsertypesUser'
status:
type: string
required:
@@ -5419,7 +5423,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesUser'
$ref: '#/components/schemas/UsertypesUser'
responses:
"200":
content:
@@ -5427,7 +5431,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/TypesUser'
$ref: '#/components/schemas/UsertypesUser'
status:
type: string
required:
@@ -5485,7 +5489,7 @@ paths:
schema:
properties:
data:
$ref: '#/components/schemas/TypesUser'
$ref: '#/components/schemas/UsertypesUser'
status:
type: string
required:
@@ -5688,7 +5692,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/TypesPostableForgotPassword'
$ref: '#/components/schemas/UsertypesPostableForgotPassword'
responses:
"204":
description: No Content

View File

@@ -100,7 +100,7 @@ This command:
3. Create a `.env` file in this directory:
```env
FRONTEND_API_ENDPOINT=http://localhost:8080
VITE_FRONTEND_API_ENDPOINT=http://localhost:8080
```
4. Start the development server:

View File

@@ -273,6 +273,7 @@ Options can be simple (direct link) or nested (with another question):
- Place logo files in `public/Logos/`
- Use SVG format
- Reference as `"/Logos/your-logo.svg"`
- **Optimize new SVGs**: Run any newly downloaded SVGs through an optimizer like [SVGOMG (svgo)](https://svgomg.net/) or use `npx svgo public/Logos/your-logo.svg` to minimise their size before committing.
### 4. Links

View File

@@ -13,7 +13,6 @@ import (
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/sqlstore"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
openfgav1 "github.com/openfga/api/proto/openfga/v1"
openfgapkgtransformer "github.com/openfga/language/pkg/go/transformer"
@@ -23,7 +22,7 @@ type provider struct {
pkgAuthzService authz.AuthZ
openfgaServer *openfgaserver.Server
licensing licensing.Licensing
store roletypes.Store
store authtypes.RoleStore
registry []authz.RegisterTypeable
}
@@ -82,23 +81,23 @@ func (provider *provider) Write(ctx context.Context, additions []*openfgav1.Tupl
return provider.openfgaServer.Write(ctx, additions, deletions)
}
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*roletypes.Role, error) {
func (provider *provider) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*authtypes.Role, error) {
return provider.pkgAuthzService.Get(ctx, orgID, id)
}
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*roletypes.Role, error) {
func (provider *provider) GetByOrgIDAndName(ctx context.Context, orgID valuer.UUID, name string) (*authtypes.Role, error) {
return provider.pkgAuthzService.GetByOrgIDAndName(ctx, orgID, name)
}
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*roletypes.Role, error) {
func (provider *provider) List(ctx context.Context, orgID valuer.UUID) ([]*authtypes.Role, error) {
return provider.pkgAuthzService.List(ctx, orgID)
}
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*roletypes.Role, error) {
func (provider *provider) ListByOrgIDAndNames(ctx context.Context, orgID valuer.UUID, names []string) ([]*authtypes.Role, error) {
return provider.pkgAuthzService.ListByOrgIDAndNames(ctx, orgID, names)
}
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*roletypes.Role, error) {
func (provider *provider) ListByOrgIDAndIDs(ctx context.Context, orgID valuer.UUID, ids []valuer.UUID) ([]*authtypes.Role, error) {
return provider.pkgAuthzService.ListByOrgIDAndIDs(ctx, orgID, ids)
}
@@ -114,7 +113,7 @@ func (provider *provider) Revoke(ctx context.Context, orgID valuer.UUID, names [
return provider.pkgAuthzService.Revoke(ctx, orgID, names, subject)
}
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*roletypes.Role) error {
func (provider *provider) CreateManagedRoles(ctx context.Context, orgID valuer.UUID, managedRoles []*authtypes.Role) error {
return provider.pkgAuthzService.CreateManagedRoles(ctx, orgID, managedRoles)
}
@@ -136,16 +135,16 @@ func (provider *provider) CreateManagedUserRoleTransactions(ctx context.Context,
return provider.Write(ctx, tuples, nil)
}
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
func (provider *provider) Create(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
return provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
return provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
}
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) (*roletypes.Role, error) {
func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) (*authtypes.Role, error) {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return nil, errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
@@ -159,10 +158,10 @@ func (provider *provider) GetOrCreate(ctx context.Context, orgID valuer.UUID, ro
}
if existingRole != nil {
return roletypes.NewRoleFromStorableRole(existingRole), nil
return authtypes.NewRoleFromStorableRole(existingRole), nil
}
err = provider.store.Create(ctx, roletypes.NewStorableRoleFromRole(role))
err = provider.store.Create(ctx, authtypes.NewStorableRoleFromRole(role))
if err != nil {
return nil, err
}
@@ -217,13 +216,13 @@ func (provider *provider) GetObjects(ctx context.Context, orgID valuer.UUID, id
return objects, nil
}
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *roletypes.Role) error {
func (provider *provider) Patch(ctx context.Context, orgID valuer.UUID, role *authtypes.Role) error {
_, err := provider.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
return provider.store.Update(ctx, orgID, roletypes.NewStorableRoleFromRole(role))
return provider.store.Update(ctx, orgID, authtypes.NewStorableRoleFromRole(role))
}
func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, name string, relation authtypes.Relation, additions, deletions []*authtypes.Object) error {
@@ -232,12 +231,12 @@ func (provider *provider) PatchObjects(ctx context.Context, orgID valuer.UUID, n
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
additionTuples, err := roletypes.GetAdditionTuples(name, orgID, relation, additions)
additionTuples, err := authtypes.GetAdditionTuples(name, orgID, relation, additions)
if err != nil {
return err
}
deletionTuples, err := roletypes.GetDeletionTuples(name, orgID, relation, deletions)
deletionTuples, err := authtypes.GetDeletionTuples(name, orgID, relation, deletions)
if err != nil {
return err
}
@@ -261,7 +260,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
return err
}
role := roletypes.NewRoleFromStorableRole(storableRole)
role := authtypes.NewRoleFromStorableRole(storableRole)
err = role.ErrIfManaged()
if err != nil {
return err
@@ -271,7 +270,7 @@ func (provider *provider) Delete(ctx context.Context, orgID valuer.UUID, id valu
}
func (provider *provider) MustGetTypeables() []authtypes.Typeable {
return []authtypes.Typeable{authtypes.TypeableRole, roletypes.TypeableResourcesRoles}
return []authtypes.Typeable{authtypes.TypeableRole, authtypes.TypeableResourcesRoles}
}
func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID valuer.UUID) ([]*openfgav1.TupleKey, error) {
@@ -283,7 +282,7 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
adminSubject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAdminRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAdminRoleName),
},
orgID,
)
@@ -298,7 +297,7 @@ func (provider *provider) getManagedRoleGrantTuples(orgID valuer.UUID, userID va
anonymousSubject,
authtypes.RelationAssignee,
[]authtypes.Selector{
authtypes.MustNewSelector(authtypes.TypeRole, roletypes.SigNozAnonymousRoleName),
authtypes.MustNewSelector(authtypes.TypeRole, authtypes.SigNozAnonymousRoleName),
},
orgID,
)

View File

@@ -2,39 +2,45 @@ module base
type organisation
relations
define read: [user, role#assignee]
define update: [user, role#assignee]
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
type user
relations
define read: [user, role#assignee]
define update: [user, role#assignee]
define delete: [user, role#assignee]
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
type serviceaccount
relations
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
type anonymous
type role
relations
define assignee: [user, anonymous]
define assignee: [user, serviceaccount, anonymous]
define read: [user, role#assignee]
define update: [user, role#assignee]
define delete: [user, role#assignee]
define read: [user, serviceaccount, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
type metaresources
relations
define create: [user, role#assignee]
define list: [user, role#assignee]
define create: [user, serviceaccount, role#assignee]
define list: [user, serviceaccount, role#assignee]
type metaresource
relations
define read: [user, anonymous, role#assignee]
define update: [user, role#assignee]
define delete: [user, role#assignee]
define read: [user, serviceaccount, anonymous, role#assignee]
define update: [user, serviceaccount, role#assignee]
define delete: [user, serviceaccount, role#assignee]
define block: [user, role#assignee]
define block: [user, serviceaccount, role#assignee]
type telemetryresource
relations
define read: [user, role#assignee]
define read: [user, serviceaccount, role#assignee]

View File

@@ -19,7 +19,6 @@ import (
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
"github.com/SigNoz/signoz/pkg/types/instrumentationtypes"
"github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
"github.com/SigNoz/signoz/pkg/types/roletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
@@ -214,7 +213,7 @@ func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.U
return module.pkgDashboardModule.Update(ctx, orgID, id, updatedBy, data, diff)
}
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, role types.Role, lock bool) error {
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, role authtypes.LegacyRole, lock bool) error {
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, role, lock)
}
@@ -224,7 +223,7 @@ func (module *module) MustGetTypeables() []authtypes.Typeable {
func (module *module) MustGetManagedRoleTransactions() map[string][]*authtypes.Transaction {
return map[string][]*authtypes.Transaction{
roletypes.SigNozAnonymousRoleName: {
authtypes.SigNozAnonymousRoleName: {
{
ID: valuer.GenerateUUID(),
Relation: authtypes.RelationRead,

View File

@@ -14,8 +14,8 @@ import (
"github.com/SigNoz/signoz/pkg/http/render"
"github.com/SigNoz/signoz/pkg/modules/user"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/types/usertypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
"go.uber.org/zap"
@@ -143,10 +143,10 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
zap.String("cloudProvider", cloudProvider),
)
newPAT, err := types.NewStorableAPIKey(
newPAT, err := usertypes.NewStorableAPIKey(
integrationPATName,
integrationUser.ID,
types.RoleViewer,
authtypes.RoleViewer,
0,
)
if err != nil {
@@ -166,16 +166,16 @@ func (ah *APIHandler) getOrCreateCloudIntegrationPAT(ctx context.Context, orgId
func (ah *APIHandler) getOrCreateCloudIntegrationUser(
ctx context.Context, orgId string, cloudProvider string,
) (*types.User, *basemodel.ApiError) {
) (*usertypes.User, *basemodel.ApiError) {
cloudIntegrationUserName := fmt.Sprintf("%s-integration", cloudProvider)
email := valuer.MustNewEmail(fmt.Sprintf("%s@signoz.io", cloudIntegrationUserName))
cloudIntegrationUser, err := types.NewUser(cloudIntegrationUserName, email, types.RoleViewer, valuer.MustNewUUID(orgId), types.UserStatusActive)
cloudIntegrationUser, err := usertypes.NewUser(cloudIntegrationUserName, email, authtypes.RoleViewer, valuer.MustNewUUID(orgId), usertypes.UserStatusActive)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration user: %w", err))
}
password := types.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue())
password := usertypes.MustGenerateFactorPassword(cloudIntegrationUser.ID.StringValue())
cloudIntegrationUser, err = ah.Signoz.Modules.User.GetOrCreateUser(ctx, cloudIntegrationUser, user.WithFactorPassword(password))
if err != nil {

View File

@@ -1,6 +1,6 @@
NODE_ENV="development"
BUNDLE_ANALYSER="true"
VITE_FRONTEND_API_ENDPOINT="http://localhost:8080/"
VITE_FRONTEND_API_ENDPOINT="http://localhost:8080"
VITE_PYLON_APP_ID="pylon-app-id"
VITE_APPCUES_APP_ID="appcess-app-id"
VITE_PYLON_IDENTITY_SECRET="pylon-identity-secret"

View File

@@ -39,7 +39,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|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana)/)',
'node_modules/(?!(lodash-es|react-dnd|core-dnd|@react-dnd|dnd-core|react-dnd-html5-backend|axios|@signozhq/design-tokens|@signozhq/table|@signozhq/calendar|@signozhq/input|@signozhq/popover|@signozhq/button|@signozhq/sonner|@signozhq/*|date-fns|d3-interpolate|d3-color|api|@codemirror|@lezer|@marijn|@grafana|nuqs)/)',
],
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
testPathIgnorePatterns: ['/node_modules/', '/public/'],

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 214 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Deno</title><path d="M1.105 18.02A11.9 11.9 0 0 1 0 12.985q0-.698.078-1.376a12 12 0 0 1 .231-1.34A12 12 0 0 1 4.025 4.02a12 12 0 0 1 5.46-2.771 12 12 0 0 1 3.428-.23c1.452.112 2.825.477 4.077 1.05a12 12 0 0 1 2.78 1.774 12.02 12.02 0 0 1 4.053 7.078A12 12 0 0 1 24 12.985q0 .454-.036.914a12 12 0 0 1-.728 3.305 12 12 0 0 1-2.38 3.875c-1.33 1.357-3.02 1.962-4.43 1.936a4.4 4.4 0 0 1-2.724-1.024c-.99-.853-1.391-1.83-1.53-2.919a5 5 0 0 1 .128-1.518c.105-.38.37-1.116.76-1.437-.455-.197-1.04-.624-1.226-.829-.045-.05-.04-.13 0-.183a.155.155 0 0 1 .177-.053c.392.134.869.267 1.372.35.66.111 1.484.25 2.317.292 2.03.1 4.153-.813 4.812-2.627s.403-3.609-1.96-4.685-3.454-2.356-5.363-3.128c-1.247-.505-2.636-.205-4.06.582-3.838 2.121-7.277 8.822-5.69 15.032a.191.191 0 0 1-.315.19 12 12 0 0 1-1.25-1.634 12 12 0 0 1-.769-1.404M11.57 6.087c.649-.051 1.214.501 1.31 1.236.13.979-.228 1.99-1.41 2.013-1.01.02-1.315-.997-1.248-1.614.066-.616.574-1.575 1.35-1.635"/></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Haystack</title><path d="M2.008 0C.9 0 0 .9 0 2.008v19.984C0 23.1.9 24 2.008 24h19.984C23.1 24 24 23.1 24 21.992V2.008C24 .9 23.1 0 21.992 0Zm9.963 3.84c3.43 0 6.21 2.763 6.21 6.17v6.488a.27.27 0 0 1-.27.268 2.423 2.423 0 0 1-2.43-2.414V10.01c0-1.927-1.572-3.488-3.51-3.488S8.547 8.085 8.547 10.01v1.608a.263.263 0 0 0 .259.268h1.54a.27.27 0 0 0 .275-.263V9.945c0-.74.604-1.341 1.35-1.341s1.35.6 1.35 1.341V20.03a.275.275 0 0 1-.28.268 2.41 2.41 0 0 1-2.42-2.404v-3.23a.275.275 0 0 0-.276-.269H8.811a.264.264 0 0 0-.264.263v1.08c0 1.333-1.175 2.414-2.517 2.414a.27.27 0 0 1-.27-.268v-7.872c0-3.408 2.78-6.171 6.21-6.171"/></svg>

After

Width:  |  Height:  |  Size: 707 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 95 88" fill="none"><path fill="#ffd21e" d="M47.21 76.5a34.75 34.75 0 1 0 0-69.5 34.75 34.75 0 0 0 0 69.5"/><path fill="#ff9d0b" d="M81.96 41.75a34.75 34.75 0 1 0-69.5 0 34.75 34.75 0 0 0 69.5 0m-73.5 0a38.75 38.75 0 1 1 77.5 0 38.75 38.75 0 0 1-77.5 0"/><path fill="#3a3b45" d="M58.5 32.3c1.28.44 1.78 3.06 3.07 2.38a5 5 0 1 0-6.76-2.07c.61 1.15 2.55-.72 3.7-.32Zm-23.55 0c-1.28.44-1.79 3.06-3.07 2.38a5 5 0 1 1 6.76-2.07c-.61 1.15-2.56-.72-3.7-.32Z"/><path fill="#ff323d" d="M46.96 56.29c9.83 0 13-8.76 13-13.26 0-2.34-1.57-1.6-4.09-.36-2.33 1.15-5.46 2.74-8.9 2.74-7.19 0-13-6.88-13-2.38s3.16 13.26 13 13.26Z"/><path fill="#3a3b45" fill-rule="evenodd" d="M39.43 54a8.7 8.7 0 0 1 5.3-4.49c.4-.12.81.57 1.24 1.28.4.68.82 1.37 1.24 1.37.45 0 .9-.68 1.33-1.35.45-.7.89-1.38 1.32-1.25a8.6 8.6 0 0 1 5 4.17c3.73-2.94 5.1-7.74 5.1-10.7 0-2.34-1.57-1.6-4.09-.36l-.14.07c-2.31 1.15-5.39 2.67-8.77 2.67s-6.45-1.52-8.77-2.67c-2.6-1.29-4.23-2.1-4.23.29 0 3.05 1.46 8.06 5.47 10.97" clip-rule="evenodd"/><path fill="#ff9d0b" d="M70.71 37a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5m-46.5 0a3.25 3.25 0 1 0 0-6.5 3.25 3.25 0 0 0 0 6.5m-6.69 11c-1.62 0-3.06.66-4.07 1.87a5.97 5.97 0 0 0-1.33 3.76 7 7 0 0 0-1.94-.3c-1.55 0-2.95.59-3.94 1.66a5.8 5.8 0 0 0-.8 7 5.3 5.3 0 0 0-1.79 2.82c-.24.9-.48 2.8.8 4.74a5.22 5.22 0 0 0-.37 5.02c1.02 2.32 3.57 4.14 8.52 6.1 3.07 1.22 5.89 2 5.91 2.01a44.3 44.3 0 0 0 10.93 1.6c5.86 0 10.05-1.8 12.46-5.34 3.88-5.69 3.33-10.9-1.7-15.92-2.77-2.78-4.62-6.87-5-7.77-.78-2.66-2.84-5.62-6.25-5.62a5.7 5.7 0 0 0-4.6 2.46c-1-1.26-1.98-2.25-2.86-2.82A7.4 7.4 0 0 0 17.52 48m0 4c.51 0 1.14.22 1.82.65 2.14 1.36 6.25 8.43 7.76 11.18.5.92 1.37 1.31 2.14 1.31 1.55 0 2.75-1.53.15-3.48-3.92-2.93-2.55-7.72-.68-8.01.08-.02.17-.02.24-.02 1.7 0 2.45 2.93 2.45 2.93s2.2 5.52 5.98 9.3c3.77 3.77 3.97 6.8 1.22 10.83-1.88 2.75-5.47 3.58-9.16 3.58-3.81 0-7.73-.9-9.92-1.46-.11-.03-13.45-3.8-11.76-7 .28-.54.75-.76 1.34-.76 2.38 0 6.7 3.54 8.57 3.54.41 0 .7-.17.83-.6.79-2.85-12.06-4.05-10.98-8.17.2-.73.71-1.02 1.44-1.02 3.14 0 10.2 5.53 11.68 5.53.11 0 .2-.03.24-.1.74-1.2.33-2.04-4.9-5.2-5.21-3.16-8.88-5.06-6.8-7.33.24-.26.58-.38 1-.38 3.17 0 10.66 6.82 10.66 6.82s2.02 2.1 3.25 2.1c.28 0 .52-.1.68-.38.86-1.46-8.06-8.22-8.56-11.01-.34-1.9.24-2.85 1.31-2.85"/><path fill="#ffd21e" d="M38.6 76.69c2.75-4.04 2.55-7.07-1.22-10.84-3.78-3.77-5.98-9.3-5.98-9.3s-.82-3.2-2.69-2.9-3.24 5.08.68 8.01c3.91 2.93-.78 4.92-2.29 2.17-1.5-2.75-5.62-9.82-7.76-11.18-2.13-1.35-3.63-.6-3.13 2.2.5 2.79 9.43 9.55 8.56 11-.87 1.47-3.93-1.71-3.93-1.71s-9.57-8.71-11.66-6.44c-2.08 2.27 1.59 4.17 6.8 7.33 5.23 3.16 5.64 4 4.9 5.2-.75 1.2-12.28-8.53-13.36-4.4-1.08 4.11 11.77 5.3 10.98 8.15-.8 2.85-9.06-5.38-10.74-2.18-1.7 3.21 11.65 6.98 11.76 7.01 4.3 1.12 15.25 3.49 19.08-2.12"/><path fill="#ff9d0b" d="M77.4 48c1.62 0 3.07.66 4.07 1.87a5.97 5.97 0 0 1 1.33 3.76 7 7 0 0 1 1.95-.3c1.55 0 2.95.59 3.94 1.66a5.8 5.8 0 0 1 .8 7 5.3 5.3 0 0 1 1.78 2.82c.24.9.48 2.8-.8 4.74a5.22 5.22 0 0 1 .37 5.02c-1.02 2.32-3.57 4.14-8.51 6.1-3.08 1.22-5.9 2-5.92 2.01a44.3 44.3 0 0 1-10.93 1.6c-5.86 0-10.05-1.8-12.46-5.34-3.88-5.69-3.33-10.9 1.7-15.92 2.78-2.78 4.63-6.87 5.01-7.77.78-2.66 2.83-5.62 6.24-5.62a5.7 5.7 0 0 1 4.6 2.46c1-1.26 1.98-2.25 2.87-2.82A7.4 7.4 0 0 1 77.4 48m0 4c-.51 0-1.13.22-1.82.65-2.13 1.36-6.25 8.43-7.76 11.18a2.43 2.43 0 0 1-2.14 1.31c-1.54 0-2.75-1.53-.14-3.48 3.91-2.93 2.54-7.72.67-8.01a1.5 1.5 0 0 0-.24-.02c-1.7 0-2.45 2.93-2.45 2.93s-2.2 5.52-5.97 9.3c-3.78 3.77-3.98 6.8-1.22 10.83 1.87 2.75 5.47 3.58 9.15 3.58 3.82 0 7.73-.9 9.93-1.46.1-.03 13.45-3.8 11.76-7-.29-.54-.75-.76-1.34-.76-2.38 0-6.71 3.54-8.57 3.54-.42 0-.71-.17-.83-.6-.8-2.85 12.05-4.05 10.97-8.17-.19-.73-.7-1.02-1.44-1.02-3.14 0-10.2 5.53-11.68 5.53-.1 0-.19-.03-.23-.1-.74-1.2-.34-2.04 4.88-5.2 5.23-3.16 8.9-5.06 6.8-7.33-.23-.26-.57-.38-.98-.38-3.18 0-10.67 6.82-10.67 6.82s-2.02 2.1-3.24 2.1a.74.74 0 0 1-.68-.38c-.87-1.46 8.05-8.22 8.55-11.01.34-1.9-.24-2.85-1.31-2.85"/><path fill="#ffd21e" d="M56.33 76.69c-2.75-4.04-2.56-7.07 1.22-10.84 3.77-3.77 5.97-9.3 5.97-9.3s.82-3.2 2.7-2.9c1.86.3 3.23 5.08-.68 8.01-3.92 2.93.78 4.92 2.28 2.17 1.51-2.75 5.63-9.82 7.76-11.18 2.13-1.35 3.64-.6 3.13 2.2-.5 2.79-9.42 9.55-8.55 11 .86 1.47 3.92-1.71 3.92-1.71s9.58-8.71 11.66-6.44-1.58 4.17-6.8 7.33c-5.23 3.16-5.63 4-4.9 5.2.75 1.2 12.28-8.53 13.36-4.4 1.08 4.11-11.76 5.3-10.97 8.15.8 2.85 9.05-5.38 10.74-2.18 1.69 3.21-11.65 6.98-11.76 7.01-4.31 1.12-15.26 3.49-19.08-2.12"/></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#76b900" viewBox="0 0 24 24"><title>NVIDIA</title><path d="M8.948 8.798v-1.43a7 7 0 0 1 .424-.018c3.922-.124 6.493 3.374 6.493 3.374s-2.774 3.851-5.75 3.851a3.7 3.7 0 0 1-1.158-.185v-4.346c1.528.185 1.837.857 2.747 2.385l2.04-1.714s-1.492-1.952-4-1.952a6 6 0 0 0-.796.035m0-4.735v2.138l.424-.027c5.45-.185 9.01 4.47 9.01 4.47s-4.08 4.964-8.33 4.964a6.5 6.5 0 0 1-1.095-.097v1.325c.3.035.61.062.91.062 3.957 0 6.82-2.023 9.593-4.408.459.371 2.34 1.263 2.73 1.652-2.633 2.208-8.772 3.984-12.253 3.984-.335 0-.653-.018-.971-.053v1.864H24V4.063zm0 10.326v1.131c-3.657-.654-4.673-4.46-4.673-4.46s1.758-1.944 4.673-2.262v1.237H8.94c-1.528-.186-2.73 1.245-2.73 1.245s.68 2.412 2.739 3.11M2.456 10.9s2.164-3.197 6.5-3.533V6.201C4.153 6.59 0 10.653 0 10.653s2.35 6.802 8.948 7.42v-1.237c-4.84-.6-6.492-5.936-6.492-5.936"/></svg>

After

Width:  |  Height:  |  Size: 865 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>Ollama</title><path d="M16.361 10.26a.9.9 0 0 0-.558.47l-.072.148.001.207c0 .193.004.217.059.353.076.193.152.312.291.448.24.238.51.3.872.205a.86.86 0 0 0 .517-.436.75.75 0 0 0 .08-.498c-.064-.453-.33-.782-.724-.897a1.1 1.1 0 0 0-.466 0m-9.203.005c-.305.096-.533.32-.65.639a1.2 1.2 0 0 0-.06.52c.057.309.31.59.598.667.362.095.632.033.872-.205.14-.136.215-.255.291-.448.055-.136.059-.16.059-.353l.001-.207-.072-.148a.9.9 0 0 0-.565-.472 1 1 0 0 0-.474.007m4.184 2c-.131.071-.223.25-.195.383.031.143.157.288.353.407.105.063.112.072.117.136.004.038-.01.146-.029.243-.02.094-.036.194-.036.222.002.074.07.195.143.253.064.052.076.054.255.059.164.005.198.001.264-.03.169-.082.212-.234.15-.525-.052-.243-.042-.28.087-.355.137-.08.281-.219.324-.314a.365.365 0 0 0-.175-.48.4.4 0 0 0-.181-.033c-.126 0-.207.03-.355.124l-.085.053-.053-.032c-.219-.13-.259-.145-.391-.143a.4.4 0 0 0-.193.032m.39-2.195c-.373.036-.475.05-.654.086a4.5 4.5 0 0 0-.951.328c-.94.46-1.589 1.226-1.787 2.114-.04.176-.045.234-.045.53 0 .294.005.357.043.524.264 1.16 1.332 2.017 2.714 2.173.3.033 1.596.033 1.896 0 1.11-.125 2.064-.727 2.493-1.571.114-.226.169-.372.22-.602.039-.167.044-.23.044-.523 0-.297-.005-.355-.045-.531-.288-1.29-1.539-2.304-3.072-2.497a7 7 0 0 0-.855-.031zm.645.937a3.3 3.3 0 0 1 1.44.514c.223.148.537.458.671.662.166.251.26.508.303.82.02.143.01.251-.043.482-.08.345-.332.705-.672.957a3 3 0 0 1-.689.348c-.382.122-.632.144-1.525.138-.582-.006-.686-.01-.853-.042q-.856-.16-1.35-.68c-.264-.28-.385-.535-.45-.946-.03-.192.025-.509.137-.776.136-.326.488-.73.836-.963.403-.269.934-.46 1.422-.512.187-.02.586-.02.773-.002m-5.503-11a1.65 1.65 0 0 0-.683.298C5.617.74 5.173 1.666 4.985 2.819c-.07.436-.119 1.04-.119 1.503 0 .544.064 1.24.155 1.721.02.107.031.202.023.208l-.187.152a5.3 5.3 0 0 0-.949 1.02 5.5 5.5 0 0 0-.94 2.339 6.6 6.6 0 0 0-.023 1.357c.091.78.325 1.438.727 2.04l.13.195-.037.064c-.269.452-.498 1.105-.605 1.732-.084.496-.095.629-.095 1.294 0 .67.009.803.088 1.266.095.555.288 1.143.503 1.534.071.128.243.393.264.407.007.003-.014.067-.046.141a7.4 7.4 0 0 0-.548 1.873 5 5 0 0 0-.071.991c0 .56.031.832.148 1.279L3.42 24h1.478l-.05-.091c-.297-.552-.325-1.575-.068-2.597.117-.472.25-.819.498-1.296l.148-.29v-.177c0-.165-.003-.184-.057-.293a.9.9 0 0 0-.194-.25 1.7 1.7 0 0 1-.385-.543c-.424-.92-.506-2.286-.208-3.451.124-.486.329-.918.544-1.154a.8.8 0 0 0 .223-.531c0-.195-.07-.355-.224-.522a3.14 3.14 0 0 1-.817-1.729c-.14-.96.114-2.005.69-2.834.563-.814 1.353-1.336 2.237-1.475.199-.033.57-.028.776.01.226.04.367.028.512-.041.179-.085.268-.19.374-.431.093-.215.165-.333.36-.576.234-.29.46-.489.822-.729.413-.27.884-.467 1.352-.561.17-.035.25-.04.569-.04s.398.005.569.04a4.07 4.07 0 0 1 1.914.997c.117.109.398.457.488.602.034.057.095.177.132.267.105.241.195.346.374.43.14.068.286.082.503.045.343-.058.607-.053.943.016 1.144.23 2.14 1.173 2.581 2.437.385 1.108.276 2.267-.296 3.153-.097.15-.193.27-.333.419-.301.322-.301.722-.001 1.053.493.539.801 1.866.708 3.036-.062.772-.26 1.463-.533 1.854a2 2 0 0 1-.224.258.9.9 0 0 0-.194.25c-.054.109-.057.128-.057.293v.178l.148.29c.248.476.38.823.498 1.295.253 1.008.231 2.01-.059 2.581a1 1 0 0 0-.044.098c0 .006.329.009.732.009h.73l.02-.074.036-.134c.019-.076.057-.3.088-.516a9 9 0 0 0 0-1.258c-.11-.875-.295-1.57-.597-2.226-.032-.074-.053-.138-.046-.141a1.4 1.4 0 0 0 .108-.152c.376-.569.607-1.284.724-2.228.031-.26.031-1.378 0-1.628-.083-.645-.182-1.082-.348-1.525a6 6 0 0 0-.329-.7l-.038-.064.131-.194c.402-.604.636-1.262.727-2.04a6.6 6.6 0 0 0-.024-1.358 5.5 5.5 0 0 0-.939-2.339 5.3 5.3 0 0 0-.95-1.02l-.186-.152a.7.7 0 0 1 .023-.208c.208-1.087.201-2.443-.017-3.503-.19-.924-.535-1.658-.98-2.082-.354-.338-.716-.482-1.15-.455-.996.059-1.8 1.205-2.116 3.01a7 7 0 0 0-.097.726c0 .036-.007.066-.015.066a1 1 0 0 1-.149-.078A4.86 4.86 0 0 0 12 3.03c-.832 0-1.687.243-2.456.698a1 1 0 0 1-.148.078c-.008 0-.015-.03-.015-.066a7 7 0 0 0-.097-.725C8.997 1.392 8.337.319 7.46.048a2 2 0 0 0-.585-.041Zm.293 1.402c.248.197.523.759.682 1.388.03.113.06.244.069.292.007.047.026.152.041.233.067.365.098.76.102 1.24l.002.475-.12.175-.118.178h-.278c-.324 0-.646.041-.954.124l-.238.06c-.033.007-.038-.003-.057-.144a8.4 8.4 0 0 1 .016-2.323c.124-.788.413-1.501.696-1.711.067-.05.079-.049.157.013m9.825-.012c.17.126.358.46.498.888.28.854.36 2.028.212 3.145-.019.14-.024.151-.057.144l-.238-.06a3.7 3.7 0 0 0-.954-.124h-.278l-.119-.178-.119-.175.002-.474c.004-.669.066-1.19.214-1.772.157-.623.434-1.185.68-1.382.078-.062.09-.063.159-.012"/></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#fff" viewBox="0 0 24 24"><title>OpenRouter</title><path d="M16.778 1.844v1.919q-.569-.026-1.138-.032-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237-2.911 2.066-2.731 1.95-4.14 2.75-.396.223-1.342.574-2.185.798-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799 1.41.798 1.228.683 4.14 2.75 2.126 1.509 4.22 2.11 6.148 2.236.88.058 1.716.041 2.555.005v1.918l7.222-4.168-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021-1.364-.09-2.417-.357-3.979-1.465-2.244-1.593-2.866-2.027-3.68-2.508.889-.518 1.449-.906 3.822-2.59 1.56-1.109 2.614-1.377 3.978-1.466.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z"/></svg>

After

Width:  |  Height:  |  Size: 685 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3.94 -1.44 438.62 432.87"><path fill="#e75225" d="M215.926 7.068c115.684.024 210.638 93.784 210.493 207.844-.148 115.793-94.713 208.252-212.912 208.169C97.95 423 4.52 329.143 4.601 213.221 4.68 99.867 99.833 7.044 215.926 7.068m-63.947 73.001c2.652 12.978.076 25.082-3.846 36.988-2.716 8.244-6.47 16.183-8.711 24.539-3.694 13.769-7.885 27.619-9.422 41.701-2.21 20.25 5.795 38.086 19.493 55.822L86.527 225.94c.11 1.978-.007 2.727.21 3.361 5.968 17.43 16.471 32.115 28.243 45.957 1.246 1.465 4.082 2.217 6.182 2.221 62.782.115 125.565.109 188.347.028 1.948-.003 4.546-.369 5.741-1.618 13.456-14.063 23.746-30.079 30.179-50.257l-66.658 12.976c4.397-8.567 9.417-16.1 12.302-24.377 9.869-28.315 5.779-55.69-8.387-81.509-11.368-20.72-21.854-41.349-16.183-66.32-12.005 11.786-16.615 26.79-19.541 42.253-2.882 15.23-4.58 30.684-6.811 46.136-.317-.467-.728-.811-.792-1.212-.258-1.621-.499-3.255-.587-4.893-1.355-25.31-6.328-49.696-16.823-72.987-6.178-13.71-12.99-27.727-6.622-44.081-4.31 2.259-8.205 4.505-10.997 7.711-8.333 9.569-11.779 21.062-12.666 33.645-.757 10.75-1.796 21.552-3.801 32.123-2.107 11.109-5.448 21.998-12.956 32.209-3.033-21.81-3.37-43.38-22.928-57.237m161.877 216.523H116.942v34.007h196.914zm-157.871 51.575c-.163 28.317 28.851 49.414 64.709 47.883 29.716-1.269 56.016-24.51 53.755-47.883z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 122.88 103.53"><path d="M5.47 0H117.4c3.01 0 5.47 2.46 5.47 5.47v92.58c0 3.01-2.46 5.47-5.47 5.47H5.47c-3.01 0-5.47-2.46-5.47-5.47V5.47C0 2.46 2.46 0 5.47 0m26.37 38.55 17.79 18.42 2.14 2.13-2.12 2.16-17.97 19.05-5.07-5 15.85-16.15L26.81 43.6zM94.1 79.41H54.69v-6.84H94.1zM38.19 9.83c3.19 0 5.78 2.59 5.78 5.78s-2.59 5.78-5.78 5.78-5.78-2.59-5.78-5.78S35 9.83 38.19 9.83m-19.24 0c3.19 0 5.78 2.59 5.78 5.78s-2.59 5.78-5.78 5.78-5.78-2.59-5.78-5.78 2.58-5.78 5.78-5.78M7.49 5.41H115.4c1.15 0 2.09.94 2.09 2.09v18.32H5.4V7.5c0-1.15.94-2.09 2.09-2.09" style="fill-rule:evenodd;clip-rule:evenodd;fill:#1668dc"/></svg>

After

Width:  |  Height:  |  Size: 687 B

View File

@@ -1,6 +1,6 @@
import { ReactChild, useCallback, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { matchPath, useLocation } from 'react-router-dom';
import { matchPath, Redirect, useLocation } from 'react-router-dom';
import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set';
import getAll from 'api/v1/user/get';
@@ -128,6 +128,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
isAdmin &&
(path === ROUTES.SETTINGS ||
path === ROUTES.ORG_SETTINGS ||
path === ROUTES.MEMBERS_SETTINGS ||
path === ROUTES.BILLING ||
path === ROUTES.MY_SETTINGS);
@@ -236,13 +237,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
useEffect(() => {
// if it is an old route navigate to the new route
if (isOldRoute) {
const redirectUrl = oldNewRoutesMapping[pathname];
const newLocation = {
...location,
pathname: redirectUrl,
};
history.replace(newLocation);
// this will be handled by the redirect component below
return;
}
@@ -296,6 +291,19 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
}
}, [isLoggedInState, pathname, user, isOldRoute, currentRoute, location]);
if (isOldRoute) {
const redirectUrl = oldNewRoutesMapping[pathname];
return (
<Redirect
to={{
pathname: redirectUrl,
search: location.search,
hash: location.hash,
}}
/>
);
}
// NOTE: disabling this rule as there is no need to have div
return <>{children}</>;
}

View File

@@ -29,7 +29,6 @@ import posthog from 'posthog-js';
import { useAppContext } from 'providers/App/App';
import { IUser } from 'providers/App/types';
import { CmdKProvider } from 'providers/cmdKProvider';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
@@ -321,6 +320,19 @@ function App(): JSX.Element {
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
beforeSend(event) {
const sessionReplayUrl = posthog.get_session_replay_url?.({
withTimestamp: true,
});
if (sessionReplayUrl) {
// eslint-disable-next-line no-param-reassign
event.contexts = {
...event.contexts,
posthog: { session_replay_url: sessionReplayUrl },
};
}
return event;
},
});
setIsSentryInitialized(true);
@@ -371,28 +383,26 @@ function App(): JSX.Element {
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AppLayout>
<PreferenceContextProvider>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</PreferenceContextProvider>
</AppLayout>
</KeyboardHotkeysProvider>
</DashboardProvider>
<KeyboardHotkeysProvider>
<AppLayout>
<PreferenceContextProvider>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</PreferenceContextProvider>
</AppLayout>
</KeyboardHotkeysProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>

View File

@@ -21,6 +21,8 @@ import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type {
AuthtypesPatchableObjectsDTO,
AuthtypesPatchableRoleDTO,
AuthtypesPostableRoleDTO,
CreateRole201,
DeleteRolePathParameters,
GetObjects200,
@@ -31,8 +33,6 @@ import type {
PatchObjectsPathParameters,
PatchRolePathParameters,
RenderErrorResponseDTO,
RoletypesPatchableRoleDTO,
RoletypesPostableRoleDTO,
} from '../sigNoz.schemas';
/**
@@ -118,14 +118,14 @@ export const invalidateListRoles = async (
* @summary Create role
*/
export const createRole = (
roletypesPostableRoleDTO: BodyType<RoletypesPostableRoleDTO>,
authtypesPostableRoleDTO: BodyType<AuthtypesPostableRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateRole201>({
url: `/api/v1/roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: roletypesPostableRoleDTO,
data: authtypesPostableRoleDTO,
signal,
});
};
@@ -137,13 +137,13 @@ export const getCreateRoleMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<RoletypesPostableRoleDTO> },
{ data: BodyType<AuthtypesPostableRoleDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<RoletypesPostableRoleDTO> },
{ data: BodyType<AuthtypesPostableRoleDTO> },
TContext
> => {
const mutationKey = ['createRole'];
@@ -157,7 +157,7 @@ export const getCreateRoleMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createRole>>,
{ data: BodyType<RoletypesPostableRoleDTO> }
{ data: BodyType<AuthtypesPostableRoleDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -170,7 +170,7 @@ export const getCreateRoleMutationOptions = <
export type CreateRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createRole>>
>;
export type CreateRoleMutationBody = BodyType<RoletypesPostableRoleDTO>;
export type CreateRoleMutationBody = BodyType<AuthtypesPostableRoleDTO>;
export type CreateRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -183,13 +183,13 @@ export const useCreateRole = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<RoletypesPostableRoleDTO> },
{ data: BodyType<AuthtypesPostableRoleDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createRole>>,
TError,
{ data: BodyType<RoletypesPostableRoleDTO> },
{ data: BodyType<AuthtypesPostableRoleDTO> },
TContext
> => {
const mutationOptions = getCreateRoleMutationOptions(options);
@@ -370,13 +370,13 @@ export const invalidateGetRole = async (
*/
export const patchRole = (
{ id }: PatchRolePathParameters,
roletypesPatchableRoleDTO: BodyType<RoletypesPatchableRoleDTO>,
authtypesPatchableRoleDTO: BodyType<AuthtypesPatchableRoleDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/roles/${id}`,
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
data: roletypesPatchableRoleDTO,
data: authtypesPatchableRoleDTO,
});
};
@@ -389,7 +389,7 @@ export const getPatchRoleMutationOptions = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<RoletypesPatchableRoleDTO>;
data: BodyType<AuthtypesPatchableRoleDTO>;
},
TContext
>;
@@ -398,7 +398,7 @@ export const getPatchRoleMutationOptions = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<RoletypesPatchableRoleDTO>;
data: BodyType<AuthtypesPatchableRoleDTO>;
},
TContext
> => {
@@ -415,7 +415,7 @@ export const getPatchRoleMutationOptions = <
Awaited<ReturnType<typeof patchRole>>,
{
pathParams: PatchRolePathParameters;
data: BodyType<RoletypesPatchableRoleDTO>;
data: BodyType<AuthtypesPatchableRoleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -429,7 +429,7 @@ export const getPatchRoleMutationOptions = <
export type PatchRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof patchRole>>
>;
export type PatchRoleMutationBody = BodyType<RoletypesPatchableRoleDTO>;
export type PatchRoleMutationBody = BodyType<AuthtypesPatchableRoleDTO>;
export type PatchRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -444,7 +444,7 @@ export const usePatchRole = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<RoletypesPatchableRoleDTO>;
data: BodyType<AuthtypesPatchableRoleDTO>;
},
TContext
>;
@@ -453,7 +453,7 @@ export const usePatchRole = <
TError,
{
pathParams: PatchRolePathParameters;
data: BodyType<RoletypesPatchableRoleDTO>;
data: BodyType<AuthtypesPatchableRoleDTO>;
},
TContext
> => {

View File

@@ -278,6 +278,13 @@ export interface AuthtypesPatchableObjectsDTO {
deletions: AuthtypesGettableObjectsDTO[] | null;
}
export interface AuthtypesPatchableRoleDTO {
/**
* @type string
*/
description: string;
}
export interface AuthtypesPostableAuthDomainDTO {
config?: AuthtypesAuthDomainConfigDTO;
/**
@@ -301,6 +308,17 @@ export interface AuthtypesPostableEmailPasswordSessionDTO {
password?: string;
}
export interface AuthtypesPostableRoleDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
}
export interface AuthtypesPostableRotateTokenDTO {
/**
* @type string
@@ -319,6 +337,39 @@ export interface AuthtypesResourceDTO {
type: string;
}
export interface AuthtypesRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
*/
type: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
/**
* @nullable
*/
@@ -2039,57 +2090,6 @@ export interface RenderErrorResponseDTO {
status: string;
}
export interface RoletypesPatchableRoleDTO {
/**
* @type string
*/
description: string;
}
export interface RoletypesPostableRoleDTO {
/**
* @type string
*/
description?: string;
/**
* @type string
*/
name: string;
}
export interface RoletypesRoleDTO {
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
description: string;
/**
* @type string
*/
id: string;
/**
* @type string
*/
name: string;
/**
* @type string
*/
orgId: string;
/**
* @type string
*/
type: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
export interface ServiceaccounttypesFactorAPIKeyDTO {
/**
* @type string
@@ -2100,7 +2100,7 @@ export interface ServiceaccounttypesFactorAPIKeyDTO {
* @type integer
* @minimum 0
*/
expires_at: number;
expiresAt: number;
/**
* @type string
*/
@@ -2113,7 +2113,7 @@ export interface ServiceaccounttypesFactorAPIKeyDTO {
* @type string
* @format date-time
*/
last_used: Date;
lastObservedAt: Date;
/**
* @type string
*/
@@ -2121,7 +2121,7 @@ export interface ServiceaccounttypesFactorAPIKeyDTO {
/**
* @type string
*/
service_account_id: string;
serviceAccountId: string;
/**
* @type string
* @format date-time
@@ -2145,7 +2145,7 @@ export interface ServiceaccounttypesPostableFactorAPIKeyDTO {
* @type integer
* @minimum 0
*/
expires_at: number;
expiresAt: number;
/**
* @type string
*/
@@ -2173,6 +2173,11 @@ export interface ServiceaccounttypesServiceAccountDTO {
* @format date-time
*/
createdAt?: Date;
/**
* @type string
* @format date-time
*/
deletedAt: Date;
/**
* @type string
*/
@@ -2188,7 +2193,7 @@ export interface ServiceaccounttypesServiceAccountDTO {
/**
* @type string
*/
orgID: string;
orgId: string;
/**
* @type array
*/
@@ -2209,7 +2214,7 @@ export interface ServiceaccounttypesUpdatableFactorAPIKeyDTO {
* @type integer
* @minimum 0
*/
expires_at: number;
expiresAt: number;
/**
* @type string
*/
@@ -2325,7 +2330,59 @@ export interface TelemetrytypesTelemetryFieldValuesDTO {
stringValues?: string[];
}
export interface TypesChangePasswordRequestDTO {
export interface TypesGettableGlobalConfigDTO {
/**
* @type string
*/
external_url?: string;
/**
* @type string
*/
ingestion_url?: string;
}
export interface TypesIdentifiableDTO {
/**
* @type string
*/
id: string;
}
export interface TypesOrganizationDTO {
/**
* @type string
*/
alias?: string;
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
displayName?: string;
/**
* @type string
*/
id: string;
/**
* @type integer
* @minimum 0
*/
key?: number;
/**
* @type string
*/
name?: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
export interface UsertypesChangePasswordRequestDTO {
/**
* @type string
*/
@@ -2340,7 +2397,7 @@ export interface TypesChangePasswordRequestDTO {
userId?: string;
}
export interface TypesGettableAPIKeyDTO {
export interface UsertypesGettableAPIKeyDTO {
/**
* @type string
* @format date-time
@@ -2350,7 +2407,7 @@ export interface TypesGettableAPIKeyDTO {
* @type string
*/
createdBy?: string;
createdByUser?: TypesUserDTO;
createdByUser?: UsertypesUserDTO;
/**
* @type integer
* @format int64
@@ -2390,32 +2447,14 @@ export interface TypesGettableAPIKeyDTO {
* @type string
*/
updatedBy?: string;
updatedByUser?: TypesUserDTO;
updatedByUser?: UsertypesUserDTO;
/**
* @type string
*/
userId?: string;
}
export interface TypesGettableGlobalConfigDTO {
/**
* @type string
*/
external_url?: string;
/**
* @type string
*/
ingestion_url?: string;
}
export interface TypesIdentifiableDTO {
/**
* @type string
*/
id: string;
}
export interface TypesInviteDTO {
export interface UsertypesInviteDTO {
/**
* @type string
* @format date-time
@@ -2456,41 +2495,7 @@ export interface TypesInviteDTO {
updatedAt?: Date;
}
export interface TypesOrganizationDTO {
/**
* @type string
*/
alias?: string;
/**
* @type string
* @format date-time
*/
createdAt?: Date;
/**
* @type string
*/
displayName?: string;
/**
* @type string
*/
id: string;
/**
* @type integer
* @minimum 0
*/
key?: number;
/**
* @type string
*/
name?: string;
/**
* @type string
* @format date-time
*/
updatedAt?: Date;
}
export interface TypesPostableAPIKeyDTO {
export interface UsertypesPostableAPIKeyDTO {
/**
* @type integer
* @format int64
@@ -2506,7 +2511,7 @@ export interface TypesPostableAPIKeyDTO {
role?: string;
}
export interface TypesPostableAcceptInviteDTO {
export interface UsertypesPostableAcceptInviteDTO {
/**
* @type string
*/
@@ -2525,14 +2530,14 @@ export interface TypesPostableAcceptInviteDTO {
token?: string;
}
export interface TypesPostableBulkInviteRequestDTO {
export interface UsertypesPostableBulkInviteRequestDTO {
/**
* @type array
*/
invites: TypesPostableInviteDTO[];
invites: UsertypesPostableInviteDTO[];
}
export interface TypesPostableForgotPasswordDTO {
export interface UsertypesPostableForgotPasswordDTO {
/**
* @type string
*/
@@ -2547,7 +2552,7 @@ export interface TypesPostableForgotPasswordDTO {
orgId: string;
}
export interface TypesPostableInviteDTO {
export interface UsertypesPostableInviteDTO {
/**
* @type string
*/
@@ -2566,7 +2571,7 @@ export interface TypesPostableInviteDTO {
role?: string;
}
export interface TypesPostableResetPasswordDTO {
export interface UsertypesPostableResetPasswordDTO {
/**
* @type string
*/
@@ -2577,7 +2582,7 @@ export interface TypesPostableResetPasswordDTO {
token?: string;
}
export interface TypesResetPasswordTokenDTO {
export interface UsertypesResetPasswordTokenDTO {
/**
* @type string
* @format date-time
@@ -2597,7 +2602,7 @@ export interface TypesResetPasswordTokenDTO {
token?: string;
}
export interface TypesStorableAPIKeyDTO {
export interface UsertypesStorableAPIKeyDTO {
/**
* @type string
* @format date-time
@@ -2642,7 +2647,7 @@ export interface TypesStorableAPIKeyDTO {
userId?: string;
}
export interface TypesUserDTO {
export interface UsertypesUserDTO {
/**
* @type string
* @format date-time
@@ -3013,7 +3018,7 @@ export type GetResetPasswordTokenPathParameters = {
id: string;
};
export type GetResetPasswordToken200 = {
data: TypesResetPasswordTokenDTO;
data: UsertypesResetPasswordTokenDTO;
/**
* @type string
*/
@@ -3032,7 +3037,7 @@ export type ListInvite200 = {
/**
* @type array
*/
data: TypesInviteDTO[];
data: UsertypesInviteDTO[];
/**
* @type string
*/
@@ -3040,7 +3045,7 @@ export type ListInvite200 = {
};
export type CreateInvite201 = {
data: TypesInviteDTO;
data: UsertypesInviteDTO;
/**
* @type string
*/
@@ -3054,7 +3059,7 @@ export type GetInvitePathParameters = {
token: string;
};
export type GetInvite200 = {
data: TypesInviteDTO;
data: UsertypesInviteDTO;
/**
* @type string
*/
@@ -3062,7 +3067,7 @@ export type GetInvite200 = {
};
export type AcceptInvite201 = {
data: TypesUserDTO;
data: UsertypesUserDTO;
/**
* @type string
*/
@@ -3110,7 +3115,7 @@ export type ListAPIKeys200 = {
/**
* @type array
*/
data: TypesGettableAPIKeyDTO[];
data: UsertypesGettableAPIKeyDTO[];
/**
* @type string
*/
@@ -3118,7 +3123,7 @@ export type ListAPIKeys200 = {
};
export type CreateAPIKey201 = {
data: TypesGettableAPIKeyDTO;
data: UsertypesGettableAPIKeyDTO;
/**
* @type string
*/
@@ -3158,7 +3163,7 @@ export type ListRoles200 = {
/**
* @type array
*/
data: RoletypesRoleDTO[];
data: AuthtypesRoleDTO[];
/**
* @type string
*/
@@ -3180,7 +3185,7 @@ export type GetRolePathParameters = {
id: string;
};
export type GetRole200 = {
data: RoletypesRoleDTO;
data: AuthtypesRoleDTO;
/**
* @type string
*/
@@ -3285,7 +3290,7 @@ export type ListUsers200 = {
/**
* @type array
*/
data: TypesUserDTO[];
data: UsertypesUserDTO[];
/**
* @type string
*/
@@ -3299,7 +3304,7 @@ export type GetUserPathParameters = {
id: string;
};
export type GetUser200 = {
data: TypesUserDTO;
data: UsertypesUserDTO;
/**
* @type string
*/
@@ -3310,7 +3315,7 @@ export type UpdateUserPathParameters = {
id: string;
};
export type UpdateUser200 = {
data: TypesUserDTO;
data: UsertypesUserDTO;
/**
* @type string
*/
@@ -3318,7 +3323,7 @@ export type UpdateUser200 = {
};
export type GetMyUser200 = {
data: TypesUserDTO;
data: UsertypesUserDTO;
/**
* @type string
*/

View File

@@ -38,18 +38,18 @@ import type {
ListUsers200,
RenderErrorResponseDTO,
RevokeAPIKeyPathParameters,
TypesChangePasswordRequestDTO,
TypesPostableAcceptInviteDTO,
TypesPostableAPIKeyDTO,
TypesPostableBulkInviteRequestDTO,
TypesPostableForgotPasswordDTO,
TypesPostableInviteDTO,
TypesPostableResetPasswordDTO,
TypesStorableAPIKeyDTO,
TypesUserDTO,
UpdateAPIKeyPathParameters,
UpdateUser200,
UpdateUserPathParameters,
UsertypesChangePasswordRequestDTO,
UsertypesPostableAcceptInviteDTO,
UsertypesPostableAPIKeyDTO,
UsertypesPostableBulkInviteRequestDTO,
UsertypesPostableForgotPasswordDTO,
UsertypesPostableInviteDTO,
UsertypesPostableResetPasswordDTO,
UsertypesStorableAPIKeyDTO,
UsertypesUserDTO,
} from '../sigNoz.schemas';
/**
@@ -58,14 +58,14 @@ import type {
*/
export const changePassword = (
{ id }: ChangePasswordPathParameters,
typesChangePasswordRequestDTO: BodyType<TypesChangePasswordRequestDTO>,
usertypesChangePasswordRequestDTO: BodyType<UsertypesChangePasswordRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/changePassword/${id}`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesChangePasswordRequestDTO,
data: usertypesChangePasswordRequestDTO,
signal,
});
};
@@ -79,7 +79,7 @@ export const getChangePasswordMutationOptions = <
TError,
{
pathParams: ChangePasswordPathParameters;
data: BodyType<TypesChangePasswordRequestDTO>;
data: BodyType<UsertypesChangePasswordRequestDTO>;
},
TContext
>;
@@ -88,7 +88,7 @@ export const getChangePasswordMutationOptions = <
TError,
{
pathParams: ChangePasswordPathParameters;
data: BodyType<TypesChangePasswordRequestDTO>;
data: BodyType<UsertypesChangePasswordRequestDTO>;
},
TContext
> => {
@@ -105,7 +105,7 @@ export const getChangePasswordMutationOptions = <
Awaited<ReturnType<typeof changePassword>>,
{
pathParams: ChangePasswordPathParameters;
data: BodyType<TypesChangePasswordRequestDTO>;
data: BodyType<UsertypesChangePasswordRequestDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -119,7 +119,7 @@ export const getChangePasswordMutationOptions = <
export type ChangePasswordMutationResult = NonNullable<
Awaited<ReturnType<typeof changePassword>>
>;
export type ChangePasswordMutationBody = BodyType<TypesChangePasswordRequestDTO>;
export type ChangePasswordMutationBody = BodyType<UsertypesChangePasswordRequestDTO>;
export type ChangePasswordMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -134,7 +134,7 @@ export const useChangePassword = <
TError,
{
pathParams: ChangePasswordPathParameters;
data: BodyType<TypesChangePasswordRequestDTO>;
data: BodyType<UsertypesChangePasswordRequestDTO>;
},
TContext
>;
@@ -143,7 +143,7 @@ export const useChangePassword = <
TError,
{
pathParams: ChangePasswordPathParameters;
data: BodyType<TypesChangePasswordRequestDTO>;
data: BodyType<UsertypesChangePasswordRequestDTO>;
},
TContext
> => {
@@ -338,14 +338,14 @@ export const invalidateListInvite = async (
* @summary Create invite
*/
export const createInvite = (
typesPostableInviteDTO: BodyType<TypesPostableInviteDTO>,
usertypesPostableInviteDTO: BodyType<UsertypesPostableInviteDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateInvite201>({
url: `/api/v1/invite`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableInviteDTO,
data: usertypesPostableInviteDTO,
signal,
});
};
@@ -357,13 +357,13 @@ export const getCreateInviteMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createInvite>>,
TError,
{ data: BodyType<TypesPostableInviteDTO> },
{ data: BodyType<UsertypesPostableInviteDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createInvite>>,
TError,
{ data: BodyType<TypesPostableInviteDTO> },
{ data: BodyType<UsertypesPostableInviteDTO> },
TContext
> => {
const mutationKey = ['createInvite'];
@@ -377,7 +377,7 @@ export const getCreateInviteMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createInvite>>,
{ data: BodyType<TypesPostableInviteDTO> }
{ data: BodyType<UsertypesPostableInviteDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -390,7 +390,7 @@ export const getCreateInviteMutationOptions = <
export type CreateInviteMutationResult = NonNullable<
Awaited<ReturnType<typeof createInvite>>
>;
export type CreateInviteMutationBody = BodyType<TypesPostableInviteDTO>;
export type CreateInviteMutationBody = BodyType<UsertypesPostableInviteDTO>;
export type CreateInviteMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -403,13 +403,13 @@ export const useCreateInvite = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createInvite>>,
TError,
{ data: BodyType<TypesPostableInviteDTO> },
{ data: BodyType<UsertypesPostableInviteDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createInvite>>,
TError,
{ data: BodyType<TypesPostableInviteDTO> },
{ data: BodyType<UsertypesPostableInviteDTO> },
TContext
> => {
const mutationOptions = getCreateInviteMutationOptions(options);
@@ -589,14 +589,14 @@ export const invalidateGetInvite = async (
* @summary Accept invite
*/
export const acceptInvite = (
typesPostableAcceptInviteDTO: BodyType<TypesPostableAcceptInviteDTO>,
usertypesPostableAcceptInviteDTO: BodyType<UsertypesPostableAcceptInviteDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<AcceptInvite201>({
url: `/api/v1/invite/accept`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableAcceptInviteDTO,
data: usertypesPostableAcceptInviteDTO,
signal,
});
};
@@ -608,13 +608,13 @@ export const getAcceptInviteMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof acceptInvite>>,
TError,
{ data: BodyType<TypesPostableAcceptInviteDTO> },
{ data: BodyType<UsertypesPostableAcceptInviteDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof acceptInvite>>,
TError,
{ data: BodyType<TypesPostableAcceptInviteDTO> },
{ data: BodyType<UsertypesPostableAcceptInviteDTO> },
TContext
> => {
const mutationKey = ['acceptInvite'];
@@ -628,7 +628,7 @@ export const getAcceptInviteMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof acceptInvite>>,
{ data: BodyType<TypesPostableAcceptInviteDTO> }
{ data: BodyType<UsertypesPostableAcceptInviteDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -641,7 +641,7 @@ export const getAcceptInviteMutationOptions = <
export type AcceptInviteMutationResult = NonNullable<
Awaited<ReturnType<typeof acceptInvite>>
>;
export type AcceptInviteMutationBody = BodyType<TypesPostableAcceptInviteDTO>;
export type AcceptInviteMutationBody = BodyType<UsertypesPostableAcceptInviteDTO>;
export type AcceptInviteMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -654,13 +654,13 @@ export const useAcceptInvite = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof acceptInvite>>,
TError,
{ data: BodyType<TypesPostableAcceptInviteDTO> },
{ data: BodyType<UsertypesPostableAcceptInviteDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof acceptInvite>>,
TError,
{ data: BodyType<TypesPostableAcceptInviteDTO> },
{ data: BodyType<UsertypesPostableAcceptInviteDTO> },
TContext
> => {
const mutationOptions = getAcceptInviteMutationOptions(options);
@@ -672,14 +672,14 @@ export const useAcceptInvite = <
* @summary Create bulk invite
*/
export const createBulkInvite = (
typesPostableBulkInviteRequestDTO: BodyType<TypesPostableBulkInviteRequestDTO>,
usertypesPostableBulkInviteRequestDTO: BodyType<UsertypesPostableBulkInviteRequestDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/invite/bulk`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableBulkInviteRequestDTO,
data: usertypesPostableBulkInviteRequestDTO,
signal,
});
};
@@ -691,13 +691,13 @@ export const getCreateBulkInviteMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data: BodyType<TypesPostableBulkInviteRequestDTO> },
{ data: BodyType<UsertypesPostableBulkInviteRequestDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data: BodyType<TypesPostableBulkInviteRequestDTO> },
{ data: BodyType<UsertypesPostableBulkInviteRequestDTO> },
TContext
> => {
const mutationKey = ['createBulkInvite'];
@@ -711,7 +711,7 @@ export const getCreateBulkInviteMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createBulkInvite>>,
{ data: BodyType<TypesPostableBulkInviteRequestDTO> }
{ data: BodyType<UsertypesPostableBulkInviteRequestDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -724,7 +724,7 @@ export const getCreateBulkInviteMutationOptions = <
export type CreateBulkInviteMutationResult = NonNullable<
Awaited<ReturnType<typeof createBulkInvite>>
>;
export type CreateBulkInviteMutationBody = BodyType<TypesPostableBulkInviteRequestDTO>;
export type CreateBulkInviteMutationBody = BodyType<UsertypesPostableBulkInviteRequestDTO>;
export type CreateBulkInviteMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -737,13 +737,13 @@ export const useCreateBulkInvite = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data: BodyType<TypesPostableBulkInviteRequestDTO> },
{ data: BodyType<UsertypesPostableBulkInviteRequestDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createBulkInvite>>,
TError,
{ data: BodyType<TypesPostableBulkInviteRequestDTO> },
{ data: BodyType<UsertypesPostableBulkInviteRequestDTO> },
TContext
> => {
const mutationOptions = getCreateBulkInviteMutationOptions(options);
@@ -841,14 +841,14 @@ export const invalidateListAPIKeys = async (
* @summary Create api key
*/
export const createAPIKey = (
typesPostableAPIKeyDTO: BodyType<TypesPostableAPIKeyDTO>,
usertypesPostableAPIKeyDTO: BodyType<UsertypesPostableAPIKeyDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateAPIKey201>({
url: `/api/v1/pats`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableAPIKeyDTO,
data: usertypesPostableAPIKeyDTO,
signal,
});
};
@@ -860,13 +860,13 @@ export const getCreateAPIKeyMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
{ data: BodyType<UsertypesPostableAPIKeyDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
{ data: BodyType<UsertypesPostableAPIKeyDTO> },
TContext
> => {
const mutationKey = ['createAPIKey'];
@@ -880,7 +880,7 @@ export const getCreateAPIKeyMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createAPIKey>>,
{ data: BodyType<TypesPostableAPIKeyDTO> }
{ data: BodyType<UsertypesPostableAPIKeyDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -893,7 +893,7 @@ export const getCreateAPIKeyMutationOptions = <
export type CreateAPIKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof createAPIKey>>
>;
export type CreateAPIKeyMutationBody = BodyType<TypesPostableAPIKeyDTO>;
export type CreateAPIKeyMutationBody = BodyType<UsertypesPostableAPIKeyDTO>;
export type CreateAPIKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -906,13 +906,13 @@ export const useCreateAPIKey = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
{ data: BodyType<UsertypesPostableAPIKeyDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createAPIKey>>,
TError,
{ data: BodyType<TypesPostableAPIKeyDTO> },
{ data: BodyType<UsertypesPostableAPIKeyDTO> },
TContext
> => {
const mutationOptions = getCreateAPIKeyMutationOptions(options);
@@ -1002,13 +1002,13 @@ export const useRevokeAPIKey = <
*/
export const updateAPIKey = (
{ id }: UpdateAPIKeyPathParameters,
typesStorableAPIKeyDTO: BodyType<TypesStorableAPIKeyDTO>,
usertypesStorableAPIKeyDTO: BodyType<UsertypesStorableAPIKeyDTO>,
) => {
return GeneratedAPIInstance<string>({
url: `/api/v1/pats/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: typesStorableAPIKeyDTO,
data: usertypesStorableAPIKeyDTO,
});
};
@@ -1021,7 +1021,7 @@ export const getUpdateAPIKeyMutationOptions = <
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
data: BodyType<UsertypesStorableAPIKeyDTO>;
},
TContext
>;
@@ -1030,7 +1030,7 @@ export const getUpdateAPIKeyMutationOptions = <
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
data: BodyType<UsertypesStorableAPIKeyDTO>;
},
TContext
> => {
@@ -1047,7 +1047,7 @@ export const getUpdateAPIKeyMutationOptions = <
Awaited<ReturnType<typeof updateAPIKey>>,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
data: BodyType<UsertypesStorableAPIKeyDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -1061,7 +1061,7 @@ export const getUpdateAPIKeyMutationOptions = <
export type UpdateAPIKeyMutationResult = NonNullable<
Awaited<ReturnType<typeof updateAPIKey>>
>;
export type UpdateAPIKeyMutationBody = BodyType<TypesStorableAPIKeyDTO>;
export type UpdateAPIKeyMutationBody = BodyType<UsertypesStorableAPIKeyDTO>;
export type UpdateAPIKeyMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -1076,7 +1076,7 @@ export const useUpdateAPIKey = <
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
data: BodyType<UsertypesStorableAPIKeyDTO>;
},
TContext
>;
@@ -1085,7 +1085,7 @@ export const useUpdateAPIKey = <
TError,
{
pathParams: UpdateAPIKeyPathParameters;
data: BodyType<TypesStorableAPIKeyDTO>;
data: BodyType<UsertypesStorableAPIKeyDTO>;
},
TContext
> => {
@@ -1098,14 +1098,14 @@ export const useUpdateAPIKey = <
* @summary Reset password
*/
export const resetPassword = (
typesPostableResetPasswordDTO: BodyType<TypesPostableResetPasswordDTO>,
usertypesPostableResetPasswordDTO: BodyType<UsertypesPostableResetPasswordDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/resetPassword`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableResetPasswordDTO,
data: usertypesPostableResetPasswordDTO,
signal,
});
};
@@ -1117,13 +1117,13 @@ export const getResetPasswordMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data: BodyType<TypesPostableResetPasswordDTO> },
{ data: BodyType<UsertypesPostableResetPasswordDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data: BodyType<TypesPostableResetPasswordDTO> },
{ data: BodyType<UsertypesPostableResetPasswordDTO> },
TContext
> => {
const mutationKey = ['resetPassword'];
@@ -1137,7 +1137,7 @@ export const getResetPasswordMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof resetPassword>>,
{ data: BodyType<TypesPostableResetPasswordDTO> }
{ data: BodyType<UsertypesPostableResetPasswordDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -1150,7 +1150,7 @@ export const getResetPasswordMutationOptions = <
export type ResetPasswordMutationResult = NonNullable<
Awaited<ReturnType<typeof resetPassword>>
>;
export type ResetPasswordMutationBody = BodyType<TypesPostableResetPasswordDTO>;
export type ResetPasswordMutationBody = BodyType<UsertypesPostableResetPasswordDTO>;
export type ResetPasswordMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -1163,13 +1163,13 @@ export const useResetPassword = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data: BodyType<TypesPostableResetPasswordDTO> },
{ data: BodyType<UsertypesPostableResetPasswordDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof resetPassword>>,
TError,
{ data: BodyType<TypesPostableResetPasswordDTO> },
{ data: BodyType<UsertypesPostableResetPasswordDTO> },
TContext
> => {
const mutationOptions = getResetPasswordMutationOptions(options);
@@ -1428,13 +1428,13 @@ export const invalidateGetUser = async (
*/
export const updateUser = (
{ id }: UpdateUserPathParameters,
typesUserDTO: BodyType<TypesUserDTO>,
usertypesUserDTO: BodyType<UsertypesUserDTO>,
) => {
return GeneratedAPIInstance<UpdateUser200>({
url: `/api/v1/user/${id}`,
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
data: typesUserDTO,
data: usertypesUserDTO,
});
};
@@ -1445,13 +1445,13 @@ export const getUpdateUserMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateUser>>,
TError,
{ pathParams: UpdateUserPathParameters; data: BodyType<TypesUserDTO> },
{ pathParams: UpdateUserPathParameters; data: BodyType<UsertypesUserDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof updateUser>>,
TError,
{ pathParams: UpdateUserPathParameters; data: BodyType<TypesUserDTO> },
{ pathParams: UpdateUserPathParameters; data: BodyType<UsertypesUserDTO> },
TContext
> => {
const mutationKey = ['updateUser'];
@@ -1465,7 +1465,7 @@ export const getUpdateUserMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateUser>>,
{ pathParams: UpdateUserPathParameters; data: BodyType<TypesUserDTO> }
{ pathParams: UpdateUserPathParameters; data: BodyType<UsertypesUserDTO> }
> = (props) => {
const { pathParams, data } = props ?? {};
@@ -1478,7 +1478,7 @@ export const getUpdateUserMutationOptions = <
export type UpdateUserMutationResult = NonNullable<
Awaited<ReturnType<typeof updateUser>>
>;
export type UpdateUserMutationBody = BodyType<TypesUserDTO>;
export type UpdateUserMutationBody = BodyType<UsertypesUserDTO>;
export type UpdateUserMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -1491,13 +1491,13 @@ export const useUpdateUser = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateUser>>,
TError,
{ pathParams: UpdateUserPathParameters; data: BodyType<TypesUserDTO> },
{ pathParams: UpdateUserPathParameters; data: BodyType<UsertypesUserDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof updateUser>>,
TError,
{ pathParams: UpdateUserPathParameters; data: BodyType<TypesUserDTO> },
{ pathParams: UpdateUserPathParameters; data: BodyType<UsertypesUserDTO> },
TContext
> => {
const mutationOptions = getUpdateUserMutationOptions(options);
@@ -1587,14 +1587,14 @@ export const invalidateGetMyUser = async (
* @summary Forgot password
*/
export const forgotPassword = (
typesPostableForgotPasswordDTO: BodyType<TypesPostableForgotPasswordDTO>,
usertypesPostableForgotPasswordDTO: BodyType<UsertypesPostableForgotPasswordDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/factor_password/forgot`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: typesPostableForgotPasswordDTO,
data: usertypesPostableForgotPasswordDTO,
signal,
});
};
@@ -1606,13 +1606,13 @@ export const getForgotPasswordMutationOptions = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof forgotPassword>>,
TError,
{ data: BodyType<TypesPostableForgotPasswordDTO> },
{ data: BodyType<UsertypesPostableForgotPasswordDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof forgotPassword>>,
TError,
{ data: BodyType<TypesPostableForgotPasswordDTO> },
{ data: BodyType<UsertypesPostableForgotPasswordDTO> },
TContext
> => {
const mutationKey = ['forgotPassword'];
@@ -1626,7 +1626,7 @@ export const getForgotPasswordMutationOptions = <
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof forgotPassword>>,
{ data: BodyType<TypesPostableForgotPasswordDTO> }
{ data: BodyType<UsertypesPostableForgotPasswordDTO> }
> = (props) => {
const { data } = props ?? {};
@@ -1639,7 +1639,7 @@ export const getForgotPasswordMutationOptions = <
export type ForgotPasswordMutationResult = NonNullable<
Awaited<ReturnType<typeof forgotPassword>>
>;
export type ForgotPasswordMutationBody = BodyType<TypesPostableForgotPasswordDTO>;
export type ForgotPasswordMutationBody = BodyType<UsertypesPostableForgotPasswordDTO>;
export type ForgotPasswordMutationError = ErrorType<RenderErrorResponseDTO>;
/**
@@ -1652,13 +1652,13 @@ export const useForgotPassword = <
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof forgotPassword>>,
TError,
{ data: BodyType<TypesPostableForgotPasswordDTO> },
{ data: BodyType<UsertypesPostableForgotPasswordDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof forgotPassword>>,
TError,
{ data: BodyType<TypesPostableForgotPasswordDTO> },
{ data: BodyType<UsertypesPostableForgotPasswordDTO> },
TContext
> => {
const mutationOptions = getForgotPasswordMutationOptions(options);

View File

@@ -1,6 +1,31 @@
.custom-time-picker {
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
gap: 4px;
.zoom-out-btn {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--foreground);
border: 1px solid var(--border);
border-radius: 2px;
box-shadow: none;
padding: 10px;
height: 33px;
&:hover:not(:disabled) {
color: var(--bg-vanilla-100);
background: var(--primary);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.timeSelection-input {
&:hover {

View File

@@ -16,6 +16,15 @@ jest.mock('react-router-dom', () => {
};
});
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: jest.fn(() => jest.fn()),
useSelector: jest.fn(() => ({
minTime: 0,
maxTime: Date.now(),
})),
}));
jest.mock('providers/Timezone', () => {
const actual = jest.requireActual('providers/Timezone');

View File

@@ -7,9 +7,11 @@ import {
useState,
} from 'react';
import { useLocation } from 'react-router-dom';
import { Button } from '@signozhq/button';
import { Input, InputRef, Popover, Tooltip } from 'antd';
import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { QueryParams } from 'constants/query';
import { DateTimeRangeType } from 'container/TopNav/CustomDateTimeModal';
import {
FixedDurationSuggestionOptions,
@@ -17,9 +19,11 @@ import {
RelativeDurationSuggestionOptions,
} from 'container/TopNav/DateTimeSelectionV2/constants';
import dayjs from 'dayjs';
import { useZoomOut } from 'hooks/useZoomOut';
import { isValidShortHandDateTimeFormat } from 'lib/getMinMax';
import { isZoomOutDisabled } from 'lib/zoomOutUtils';
import { defaultTo, isFunction, noop } from 'lodash-es';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { ChevronDown, ChevronUp, ZoomOut } from 'lucide-react';
import { useTimezone } from 'providers/Timezone';
import { getTimeDifference, validateEpochRange } from 'utils/epochUtils';
import { popupContainer } from 'utils/selectPopupContainer';
@@ -66,6 +70,8 @@ interface CustomTimePickerProps {
showRecentlyUsed?: boolean;
minTime: number;
maxTime: number;
/** When true, zoom-out button is hidden (e.g. in drawer/modal time selection) */
isModalTimeSelection?: boolean;
}
function CustomTimePicker({
@@ -88,6 +94,7 @@ function CustomTimePicker({
showRecentlyUsed = true,
minTime,
maxTime,
isModalTimeSelection = false,
}: CustomTimePickerProps): JSX.Element {
const [
selectedTimePlaceholderValue,
@@ -116,6 +123,14 @@ function CustomTimePicker({
const [isOpenedFromFooter, setIsOpenedFromFooter] = useState(false);
const durationMs = (maxTime - minTime) / 1e6;
const zoomOutDisabled = showLiveLogs || isZoomOutDisabled(durationMs);
const handleZoomOut = useZoomOut({
isDisabled: zoomOutDisabled,
urlParamsToDelete: [QueryParams.activeLogId],
});
// function to get selected time in Last 1m, Last 2h, Last 3d, Last 4w format
// 1m, 2h, 3d, 4w -> Last 1 minute, Last 2 hours, Last 3 days, Last 4 weeks
const getSelectedTimeRangeLabelInRelativeFormat = (
@@ -282,7 +297,11 @@ function CustomTimePicker({
resetErrorStatus();
};
const handleInputPressEnter = (): void => {
const handleInputPressEnter = (
event?: React.KeyboardEvent<HTMLInputElement>,
): void => {
event?.preventDefault();
event?.stopPropagation();
// check if the entered time is in the format of 1m, 2h, 3d, 4w
const isTimeDurationShortHandFormat = /^(\d+)([mhdw])$/.test(inputValue);
@@ -631,6 +650,23 @@ function CustomTimePicker({
/>
</Popover>
</Tooltip>
{!showLiveLogs && !isModalTimeSelection && (
<Tooltip
title={
zoomOutDisabled ? 'Zoom out time range is limited to 1 month' : 'Zoom out'
}
>
<span>
<Button
className="zoom-out-btn"
onClick={handleZoomOut}
disabled={zoomOutDisabled}
data-testid="zoom-out-btn"
prefixIcon={<ZoomOut size={14} />}
/>
</span>
</Tooltip>
)}
</div>
);
}

View File

@@ -0,0 +1,169 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryParams } from 'constants/query';
import { GlobalReducer } from 'types/reducer/globalTime';
import CustomTimePicker from '../CustomTimePicker';
const MS_PER_MIN = 60 * 1000;
const NOW_MS = 1705312800000;
const mockDispatch = jest.fn();
const mockSafeNavigate = jest.fn();
const mockUrlQueryDelete = jest.fn();
const mockUrlQuerySet = jest.fn();
interface MockAppState {
globalTime: Pick<GlobalReducer, 'minTime' | 'maxTime'>;
}
jest.mock('react-redux', () => ({
useDispatch: (): jest.Mock => mockDispatch,
useSelector: (selector: (state: MockAppState) => unknown): unknown => {
const mockState: MockAppState = {
globalTime: {
minTime: (NOW_MS - 15 * MS_PER_MIN) * 1e6,
maxTime: NOW_MS * 1e6,
},
};
return selector(mockState);
},
}));
jest.mock('hooks/useSafeNavigate', () => ({
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
safeNavigate: mockSafeNavigate,
}),
}));
interface MockUrlQuery {
delete: typeof mockUrlQueryDelete;
set: typeof mockUrlQuerySet;
get: () => null;
toString: () => string;
}
jest.mock('hooks/useUrlQuery', () => ({
__esModule: true,
default: (): MockUrlQuery => ({
delete: mockUrlQueryDelete,
set: mockUrlQuerySet,
get: (): null => null,
toString: (): string => 'relativeTime=45m',
}),
}));
jest.mock('providers/Timezone', () => ({
useTimezone: (): { timezone: { value: string; offset: string } } => ({
timezone: { value: 'UTC', offset: 'UTC' },
}),
}));
jest.mock('react-router-dom', () => ({
useLocation: (): { pathname: string } => ({ pathname: '/logs-explorer' }),
}));
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const now = Date.now();
const defaultProps = {
onSelect: jest.fn(),
onError: jest.fn(),
selectedValue: '15m',
selectedTime: '15m',
onValidCustomDateChange: jest.fn(),
open: false,
setOpen: jest.fn(),
items: [
{ value: '15m', label: 'Last 15 minutes' },
{ value: '1h', label: 'Last 1 hour' },
],
minTime: (now - 15 * 60 * 1000) * 1e6,
maxTime: now * 1e6,
};
describe('CustomTimePicker - zoom out button', () => {
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(Date, 'now').mockReturnValue(NOW_MS);
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should render zoom out button when showLiveLogs is false', () => {
render(<CustomTimePicker {...defaultProps} showLiveLogs={false} />);
expect(screen.getByTestId('zoom-out-btn')).toBeInTheDocument();
});
it('should not render zoom out button when showLiveLogs is true', () => {
render(<CustomTimePicker {...defaultProps} showLiveLogs={true} />);
expect(screen.queryByTestId('zoom-out-btn')).not.toBeInTheDocument();
});
it('should not render zoom out button when isModalTimeSelection is true', () => {
render(
<CustomTimePicker
{...defaultProps}
showLiveLogs={false}
isModalTimeSelection={true}
/>,
);
expect(screen.queryByTestId('zoom-out-btn')).not.toBeInTheDocument();
});
it('should call handleZoomOut when zoom out button is clicked', async () => {
render(<CustomTimePicker {...defaultProps} showLiveLogs={false} />);
const zoomOutBtn = screen.getByTestId('zoom-out-btn');
await userEvent.click(zoomOutBtn);
expect(mockDispatch).toHaveBeenCalled();
expect(mockUrlQuerySet).toHaveBeenCalledWith(QueryParams.relativeTime, '45m');
expect(mockSafeNavigate).toHaveBeenCalledWith(
expect.stringMatching(/\/logs-explorer\?relativeTime=45m/),
);
});
it('should use real ladder logic: 15m range zooms to 45m preset and updates URL', async () => {
render(<CustomTimePicker {...defaultProps} showLiveLogs={false} />);
const zoomOutBtn = screen.getByTestId('zoom-out-btn');
await userEvent.click(zoomOutBtn);
expect(mockUrlQueryDelete).toHaveBeenCalledWith(QueryParams.startTime);
expect(mockUrlQueryDelete).toHaveBeenCalledWith(QueryParams.endTime);
expect(mockUrlQuerySet).toHaveBeenCalledWith(QueryParams.relativeTime, '45m');
expect(mockSafeNavigate).toHaveBeenCalledWith(
expect.stringMatching(/\/logs-explorer\?relativeTime=45m/),
);
expect(mockDispatch).toHaveBeenCalled();
});
it('should delete activeLogId when zoom out is clicked', async () => {
render(<CustomTimePicker {...defaultProps} showLiveLogs={false} />);
const zoomOutBtn = screen.getByTestId('zoom-out-btn');
await userEvent.click(zoomOutBtn);
expect(mockUrlQueryDelete).toHaveBeenCalledWith(QueryParams.activeLogId);
});
it('should disable zoom button when time range is >= 1 month', () => {
const now = Date.now();
render(
<CustomTimePicker
{...defaultProps}
minTime={(now - 31 * MS_PER_DAY) * 1e6}
maxTime={now * 1e6}
showLiveLogs={false}
/>,
);
const zoomOutBtn = screen.getByTestId('zoom-out-btn');
expect(zoomOutBtn).toBeDisabled();
});
});

View File

@@ -361,6 +361,58 @@ describe('CheckboxFilter - User Flows', () => {
expect(filtersForServiceName).toHaveLength(0);
});
it('should match filter when query uses resource. prefix (resource.service.name matches service.name)', async () => {
// Filter config uses unprefixed key (service.name)
// Query has filter with resource. prefix (resource.service.name)
// Checkbox should recognize the match and show checked state
mockUseQueryBuilder.mockReturnValue({
lastUsedQuery: 0,
currentQuery: {
builder: {
queryData: [
{
filters: {
items: [
{
key: {
key: 'resource.service.name',
dataType: DataTypes.String,
type: 'resource',
},
op: 'in',
value: [OTEL_DEMO],
},
],
op: 'AND',
},
},
],
},
},
redirectWithQueryBuilderData: jest.fn(),
} as any);
const mockFilter = createMockFilter({ defaultOpen: false });
render(
<CheckboxFilter
filter={mockFilter}
source={QuickFiltersSource.LOGS_EXPLORER}
/>,
);
// Filter should auto-open because it has active filters (key match via prefix stripping)
await waitFor(() => {
expect(screen.getByPlaceholderText('Filter values')).toBeInTheDocument();
});
// otel-demo should be checked (filter uses resource.service.name IN [otel-demo])
// Checked items are sorted to the top, so otel-demo is first
const checkboxes = screen.getAllByRole('checkbox');
expect(checkboxes[0]).toBeChecked();
expect(screen.getByText(OTEL_DEMO)).toBeInTheDocument();
});
it('should extend an existing IN filter when checking an additional value', async () => {
const redirectWithQueryBuilderData = jest.fn();

View File

@@ -18,7 +18,7 @@ import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues'
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
import { cloneDeep, isArray, isFunction } from 'lodash-es';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
@@ -26,6 +26,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
import { isKeyMatch } from './utils';
import './Checkbox.styles.scss';
@@ -84,7 +85,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
currentQuery.builder.queryData?.[
activeQueryIndex
]?.filters?.items?.some((item) =>
isEqual(item.key?.key, filter.attributeKey.key),
isKeyMatch(item.key?.key, filter.attributeKey.key),
),
[currentQuery.builder.queryData, activeQueryIndex, filter.attributeKey.key],
);
@@ -189,7 +190,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const filterSync = currentQuery?.builder.queryData?.[
activeQueryIndex
]?.filters?.items.find((item) =>
isEqual(item.key?.key, filter.attributeKey.key),
isKeyMatch(item.key?.key, filter.attributeKey.key),
);
if (filterSync) {
@@ -236,7 +237,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
(currentQuery?.builder?.queryData?.[
activeQueryIndex
]?.filters?.items?.filter((item) =>
isEqual(item.key?.key, filter.attributeKey.key),
isKeyMatch(item.key?.key, filter.attributeKey.key),
)?.length || 0) > 1,
[currentQuery?.builder?.queryData, activeQueryIndex, filter.attributeKey],
@@ -280,7 +281,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
items:
idx === activeQueryIndex
? item.filters?.items?.filter(
(fil) => !isEqual(fil.key?.key, filter.attributeKey.key),
(fil) => !isKeyMatch(fil.key?.key, filter.attributeKey.key),
) || []
: [...(item.filters?.items || [])],
op: item.filters?.op || 'AND',
@@ -313,7 +314,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
: 'Only'
: 'Only';
query.filters.items = query.filters.items.filter(
(q) => !isEqual(q.key?.key, filter.attributeKey.key),
(q) => !isKeyMatch(q.key?.key, filter.attributeKey.key),
);
if (query.filter?.expression) {
@@ -335,13 +336,13 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
} else if (query?.filters?.items) {
if (
query.filters?.items?.some((item) =>
isEqual(item.key?.key, filter.attributeKey.key),
isKeyMatch(item.key?.key, filter.attributeKey.key),
)
) {
// if there is already a running filter for the current attribute key then
// we split the cases by which particular operator is present right now!
const currentFilter = query.filters?.items?.find((q) =>
isEqual(q.key?.key, filter.attributeKey.key),
isKeyMatch(q.key?.key, filter.attributeKey.key),
);
if (currentFilter) {
const runningOperator = currentFilter?.op;
@@ -356,7 +357,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [...currentFilter.value, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -368,7 +369,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -385,11 +386,11 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
if (newFilter.value.length === 0) {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
);
} else {
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -398,7 +399,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
} else {
// if not an array remove the whole thing altogether!
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
);
}
}
@@ -414,7 +415,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [...currentFilter.value, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -426,7 +427,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -441,7 +442,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
};
if (newFilter.value.length === 0) {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
);
if (query.filter?.expression) {
query.filter.expression = removeKeysFromExpression(
@@ -451,7 +452,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
}
} else {
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
@@ -469,7 +470,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
);
}
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
);
}
}
@@ -482,14 +483,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
});
} else if (!checked) {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
);
}
break;
@@ -501,14 +502,14 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
value: [currentFilter.value as string, value],
};
query.filters.items = query.filters.items.map((item) => {
if (isEqual(item.key?.key, filter.attributeKey.key)) {
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
return newFilter;
}
return item;
});
} else if (checked) {
query.filters.items = query.filters.items.filter(
(item) => !isEqual(item.key?.key, filter.attributeKey.key),
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
);
}
break;

View File

@@ -0,0 +1,41 @@
/**
* These prefixes are added to attribute keys based on their context.
*/
export const FIELD_CONTEXT_PREFIXES = [
'metric',
'log',
'span',
'trace',
'resource',
'scope',
'attribute',
'event',
'body',
];
/**
* Removes the field context prefix from a key to get the base key name.
* Example: 'resource.service.name' -> 'service.name'
* Example: 'attribute.http.method' -> 'http.method'
*/
export function getKeyWithoutPrefix(key: string | undefined): string {
if (!key) {
return '';
}
const prefixPattern = new RegExp(
`^(${FIELD_CONTEXT_PREFIXES.join('|')})\\.`,
'i',
);
return key.replace(prefixPattern, '').trim();
}
/**
* Compares two keys by their base name (without prefix).
* This ensures that 'service.name' matches 'resource.service.name'
*/
export function isKeyMatch(
itemKey: string | undefined,
filterKey: string | undefined,
): boolean {
return getKeyWithoutPrefix(itemKey) === getKeyWithoutPrefix(filterKey);
}

View File

@@ -55,4 +55,5 @@ export enum QueryParams {
source = 'source',
showClassicCreateAlertsPage = 'showClassicCreateAlertsPage',
isTestAlert = 'isTestAlert',
yAxisUnit = 'yAxisUnit',
}

View File

@@ -1,5 +1,6 @@
// ** Helpers
import { MetrictypesTypeDTO } from 'api/generated/services/sigNoz.schemas';
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
import { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
@@ -548,3 +549,49 @@ export const DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY: Record<
[DataTypes.ArrayBool]: 'boolAttributeValues',
[DataTypes.EMPTY]: 'stringAttributeValues',
};
export const listViewInitialLogQuery: Query = {
...initialQueriesMap.logs,
builder: {
...initialQueriesMap.logs.builder,
queryData: [
{
...initialQueriesMap.logs.builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 100,
},
],
},
};
export const PANEL_TYPES_INITIAL_QUERY: Record<PANEL_TYPES, Query> = {
[PANEL_TYPES.TIME_SERIES]: initialQueriesMap.metrics,
[PANEL_TYPES.VALUE]: initialQueriesMap.metrics,
[PANEL_TYPES.TABLE]: initialQueriesMap.metrics,
[PANEL_TYPES.LIST]: listViewInitialLogQuery,
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
[PANEL_TYPES.HISTOGRAM]: initialQueriesMap.metrics,
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
};
export const listViewInitialTraceQuery: Query = {
// it should be the above commented query
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 10,
selectColumns: defaultTraceSelectedColumns,
},
],
},
};

View File

@@ -30,14 +30,15 @@ export default function CustomDomainEditModal({
onClearError,
onSubmit,
}: CustomDomainEditModalProps): JSX.Element {
const [value, setValue] = useState(customDomainSubdomain ?? '');
const initialSubdomain = customDomainSubdomain ?? '';
const [value, setValue] = useState(initialSubdomain);
const [validationError, setValidationError] = useState<string | null>(null);
useEffect(() => {
if (isOpen) {
setValue(customDomainSubdomain ?? '');
setValue(initialSubdomain);
}
}, [isOpen, customDomainSubdomain]);
}, [isOpen, initialSubdomain]);
const handleClose = (): void => {
setValidationError(null);
@@ -58,6 +59,11 @@ export default function CustomDomainEditModal({
};
const handleSubmit = (): void => {
if (value === initialSubdomain) {
setValidationError('Input is unchanged');
return;
}
if (!value) {
setValidationError('This field is required');
return;
@@ -84,7 +90,7 @@ export default function CustomDomainEditModal({
const hasError = Boolean(errorMessage);
const statusIcon = ((): JSX.Element => {
const statusIcon = ((): JSX.Element | null => {
if (isLoading) {
return (
<LoaderCircle size={16} className="animate-spin edit-modal-status-icon" />
@@ -95,7 +101,9 @@ export default function CustomDomainEditModal({
return <CircleAlert size={16} color={Color.BG_CHERRY_500} />;
}
return <CircleCheck size={16} color={Color.BG_FOREST_500} />;
return value && value.length >= 3 ? (
<CircleCheck size={16} color={Color.BG_FOREST_500} />
) : null;
})();
return (
@@ -189,7 +197,7 @@ export default function CustomDomainEditModal({
color="primary"
className="edit-modal-apply-btn"
onClick={handleSubmit}
disabled={isLoading}
disabled={isLoading || value === initialSubdomain}
loading={isLoading}
>
Apply Changes

View File

@@ -81,6 +81,10 @@
padding-left: 26px;
}
.custom-domain-card-meta-row.workspace-name-hidden {
padding-left: 0;
}
.custom-domain-card-meta-timezone {
display: inline-flex;
align-items: center;
@@ -117,32 +121,6 @@
background: var(--l2-border);
margin: 0;
}
.custom-domain-card-bottom {
display: flex;
align-items: center;
gap: var(--spacing-5);
padding: var(--padding-3);
}
.custom-domain-card-license {
color: var(--l1-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
}
.custom-domain-plan-badge {
display: inline-flex;
align-items: center;
padding: 0 2px;
border-radius: 2px;
background: var(--l2-background);
color: var(--l2-foreground);
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
}
}
.workspace-url-trigger {

View File

@@ -69,8 +69,9 @@ function DomainUpdateToast({
}
export default function CustomDomainSettings(): JSX.Element {
const { org, activeLicense } = useAppContext();
const { org } = useAppContext();
const { timezone } = useTimezone();
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [isPollingEnabled, setIsPollingEnabled] = useState(false);
const [hosts, setHosts] = useState<ZeustypesHostDTO[] | null>(null);
@@ -175,7 +176,8 @@ export default function CustomDomainSettings(): JSX.Element {
[hosts, activeHost],
);
const planName = activeLicense?.plan?.name;
const workspaceName =
org?.[0]?.displayName || customDomainSubdomain || activeHost?.name;
if (isLoadingHosts) {
return (
@@ -191,106 +193,98 @@ export default function CustomDomainSettings(): JSX.Element {
return (
<>
<div className="custom-domain-card">
<div className="custom-domain-card-top">
<div className="custom-domain-card-info">
<div className="custom-domain-card-top">
<div className="custom-domain-card-info">
{!!workspaceName && (
<div className="custom-domain-card-name-row">
<span className="beacon" />
<span className="custom-domain-card-org-name">
{org?.[0]?.displayName ? org?.[0]?.displayName : customDomainSubdomain}
</span>
<span className="custom-domain-card-org-name">{workspaceName}</span>
</div>
)}
<div className="custom-domain-card-meta-row">
<Dropdown
trigger={['click']}
dropdownRender={(): JSX.Element => (
<div className="workspace-url-dropdown">
<span className="workspace-url-dropdown-header">
All Workspace URLs
</span>
<div className="workspace-url-dropdown-divider" />
{sortedHosts.map((host) => {
const isActive = host.name === activeHost?.name;
return (
<a
key={host.name}
href={host.url}
target="_blank"
rel="noopener noreferrer"
className={`workspace-url-dropdown-item${
isActive ? ' workspace-url-dropdown-item--active' : ''
}`}
>
<span className="workspace-url-dropdown-item-label">
{stripProtocol(host.url ?? '')}
</span>
{isActive ? (
<Check size={14} className="workspace-url-dropdown-item-check" />
) : (
<ExternalLink
size={12}
className="workspace-url-dropdown-item-external"
/>
)}
</a>
);
})}
</div>
)}
>
<Button
type="button"
size="xs"
className="workspace-url-trigger"
disabled={isFetchingHosts}
>
<Link2 size={12} />
<span>{stripProtocol(activeHost?.url ?? '')}</span>
<ChevronDown size={12} />
</Button>
</Dropdown>
<span className="custom-domain-card-meta-timezone">
<Clock size={11} />
{timezone.offset}
</span>
</div>
</div>
<Button
variant="solid"
size="sm"
className="custom-domain-edit-button"
prefixIcon={<FilePenLine size={12} />}
disabled={isFetchingHosts || isPollingEnabled}
onClick={(): void => setIsEditModalOpen(true)}
<div
className={`custom-domain-card-meta-row ${
!workspaceName ? 'workspace-name-hidden' : ''
}`}
>
Edit workspace link
</Button>
<Dropdown
trigger={['click']}
dropdownRender={(): JSX.Element => (
<div className="workspace-url-dropdown">
<span className="workspace-url-dropdown-header">
All Workspace URLs
</span>
<div className="workspace-url-dropdown-divider" />
{sortedHosts.map((host) => {
const isActive = host.name === activeHost?.name;
return (
<a
key={host.name}
href={host.url}
target="_blank"
rel="noopener noreferrer"
className={`workspace-url-dropdown-item${
isActive ? ' workspace-url-dropdown-item--active' : ''
}`}
>
<span className="workspace-url-dropdown-item-label">
{stripProtocol(host.url ?? '')}
</span>
{isActive ? (
<Check size={14} className="workspace-url-dropdown-item-check" />
) : (
<ExternalLink
size={12}
className="workspace-url-dropdown-item-external"
/>
)}
</a>
);
})}
</div>
)}
>
<Button
type="button"
size="xs"
className="workspace-url-trigger"
disabled={isFetchingHosts}
>
<Link2 size={12} />
<span>{stripProtocol(activeHost?.url ?? '')}</span>
<ChevronDown size={12} />
</Button>
</Dropdown>
<span className="custom-domain-card-meta-timezone">
<Clock size={11} />
{timezone.offset}
</span>
</div>
</div>
{isPollingEnabled && (
<Callout
type="info"
showIcon
className="custom-domain-callout"
size="small"
icon={<SolidAlertCircle size={13} color="primary" />}
message={`Updating your URL to ⎯ ${customDomainSubdomain}.${dnsSuffix}. This may take a few mins.`}
/>
)}
<div className="custom-domain-card-divider" />
<div className="custom-domain-card-bottom">
<span className="beacon" />
<span className="custom-domain-card-license">
{planName && <code className="custom-domain-plan-badge">{planName}</code>}{' '}
license is currently active
</span>
</div>
<Button
variant="solid"
size="sm"
className="custom-domain-edit-button"
prefixIcon={<FilePenLine size={12} />}
disabled={isFetchingHosts || isPollingEnabled}
onClick={(): void => setIsEditModalOpen(true)}
>
Edit workspace link
</Button>
</div>
{isPollingEnabled && (
<Callout
type="info"
showIcon
className="custom-domain-callout"
size="small"
icon={<SolidAlertCircle size={13} color="primary" />}
message={`Updating your URL to ⎯ ${customDomainSubdomain}.${dnsSuffix}. This may take a few mins.`}
/>
)}
<CustomDomainEditModal
isOpen={isEditModalOpen}
onClose={(): void => setIsEditModalOpen(false)}

View File

@@ -239,4 +239,87 @@ describe('CustomDomainSettings', () => {
const { container } = render(toastRenderer('test-id'));
expect(container).toHaveTextContent(/myteam\.test\.cloud/i);
});
describe('Workspace Name rendering', () => {
it('renders org displayName when available from appContext', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
render(<CustomDomainSettings />, undefined, {
appContextOverrides: {
org: [{ id: 'xyz', displayName: 'My Org Name', createdAt: 0 }],
},
});
expect(await screen.findByText('My Org Name')).toBeInTheDocument();
});
it('falls back to customDomainSubdomain when org displayName is missing', async () => {
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(mockHostsResponse)),
),
);
render(<CustomDomainSettings />, undefined, {
appContextOverrides: { org: [] },
});
expect(await screen.findByText('custom-host')).toBeInTheDocument();
});
it('falls back to activeHost.name when neither org name nor custom domain exists', async () => {
const onlyDefaultHostResponse = {
...mockHostsResponse,
data: {
...mockHostsResponse.data,
hosts: mockHostsResponse.data.hosts
? [mockHostsResponse.data.hosts[0]]
: [],
},
};
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(onlyDefaultHostResponse)),
),
);
render(<CustomDomainSettings />, undefined, {
appContextOverrides: { org: [] },
});
// 'accepted-starfish' is the default host's name
expect(await screen.findByText('accepted-starfish')).toBeInTheDocument();
});
it('does not render the card name row if workspaceName is totally falsy', async () => {
const emptyHostsResponse = {
...mockHostsResponse,
data: {
...mockHostsResponse.data,
hosts: [],
},
};
server.use(
rest.get(ZEUS_HOSTS_ENDPOINT, (_, res, ctx) =>
res(ctx.status(200), ctx.json(emptyHostsResponse)),
),
);
const { container } = render(<CustomDomainSettings />, undefined, {
appContextOverrides: { org: [] },
});
await screen.findByRole('button', { name: /edit workspace link/i });
expect(
container.querySelector('.custom-domain-card-name-row'),
).not.toBeInTheDocument();
});
});
});

View File

@@ -1,50 +0,0 @@
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { defaultTraceSelectedColumns } from 'container/OptionsMenu/constants';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
export const PANEL_TYPES_INITIAL_QUERY = {
[PANEL_TYPES.TIME_SERIES]: initialQueriesMap.metrics,
[PANEL_TYPES.VALUE]: initialQueriesMap.metrics,
[PANEL_TYPES.TABLE]: initialQueriesMap.metrics,
[PANEL_TYPES.LIST]: initialQueriesMap.logs,
[PANEL_TYPES.TRACE]: initialQueriesMap.traces,
[PANEL_TYPES.BAR]: initialQueriesMap.metrics,
[PANEL_TYPES.PIE]: initialQueriesMap.metrics,
[PANEL_TYPES.HISTOGRAM]: initialQueriesMap.metrics,
[PANEL_TYPES.EMPTY_WIDGET]: initialQueriesMap.metrics,
};
export const listViewInitialLogQuery: Query = {
...initialQueriesMap.logs,
builder: {
...initialQueriesMap.logs.builder,
queryData: [
{
...initialQueriesMap.logs.builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 100,
},
],
},
};
export const listViewInitialTraceQuery: Query = {
// it should be the above commented query
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: [
{
...initialQueriesMap.traces.builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 10,
selectColumns: defaultTraceSelectedColumns,
},
],
},
};

View File

@@ -1,94 +0,0 @@
import { Card, Modal } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { LogsAggregatorOperator } from 'types/common/queryBuilder';
import { v4 as uuid } from 'uuid';
import { PANEL_TYPES_INITIAL_QUERY } from './constants';
import menuItems from './menuItems';
import { Text } from './styles';
import './ComponentSlider.styles.scss';
function DashboardGraphSlider(): JSX.Element {
const { handleToggleDashboardSlider, isDashboardSliderOpen } = useDashboard();
const onClickHandler = (name: PANEL_TYPES) => (): void => {
const id = uuid();
handleToggleDashboardSlider(false);
logEvent('Dashboard Detail: New panel type selected', {
// dashboardId: '',
// dashboardName: '',
// numberOfPanels: 0, // todo - at this point we don't know these attributes
panelType: name,
widgetId: id,
});
const queryParamsLog = {
graphType: name,
widgetId: id,
[QueryParams.compositeQuery]: JSON.stringify({
...PANEL_TYPES_INITIAL_QUERY[name],
builder: {
...PANEL_TYPES_INITIAL_QUERY[name].builder,
queryData: [
{
...PANEL_TYPES_INITIAL_QUERY[name].builder.queryData[0],
aggregateOperator: LogsAggregatorOperator.NOOP,
orderBy: [{ columnName: 'timestamp', order: 'desc' }],
offset: 0,
pageSize: 100,
},
],
},
}),
};
const queryParams = {
graphType: name,
widgetId: id,
[QueryParams.compositeQuery]: JSON.stringify(
PANEL_TYPES_INITIAL_QUERY[name],
),
};
if (name === PANEL_TYPES.LIST) {
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParamsLog)}`,
);
} else {
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
);
}
};
const handleCardClick = (panelType: PANEL_TYPES): void => {
onClickHandler(panelType)();
};
return (
<Modal
open={isDashboardSliderOpen}
onCancel={(): void => {
handleToggleDashboardSlider(false);
}}
rootClassName="graph-selection"
footer={null}
title="New Panel"
>
<div className="panel-selection">
{menuItems.map(({ name, icon, display }) => (
<Card onClick={(): void => handleCardClick(name)} id={name} key={name}>
{icon}
<Text>{display}</Text>
</Card>
))}
</div>
</Modal>
);
}
export default DashboardGraphSlider;

View File

@@ -1,41 +0,0 @@
import { Card as CardComponent, Typography } from 'antd';
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
justify-content: right;
gap: 8px;
margin-bottom: 12px;
`;
export const Card = styled(CardComponent)`
min-height: 80px;
min-width: 120px;
overflow-y: auto;
cursor: pointer;
transition: transform 0.2s;
.ant-card-body {
padding: 12px;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
.ant-typography {
font-size: 12px;
font-weight: 600;
}
}
&:hover {
transform: scale(1.05);
border: 1px solid var(--bg-robin-400);
}
`;
export const Text = styled(Typography)`
text-align: center;
margin-top: 1rem;
`;

View File

@@ -34,11 +34,6 @@ const mockSafeNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: jest.fn(),
useRouteMatch: jest.fn().mockReturnValue({
params: {
dashboardId: 4,
},
}),
}));
jest.mock(
@@ -69,7 +64,7 @@ describe('Dashboard landing page actions header tests', () => {
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const { getByTestId } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardProvider>
<DashboardProvider dashboardId="4">
<DashboardDescription
handle={{
active: false,
@@ -110,7 +105,7 @@ describe('Dashboard landing page actions header tests', () => {
);
const { getByTestId } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardProvider>
<DashboardProvider dashboardId="4">
<DashboardDescription
handle={{
active: false,
@@ -149,7 +144,7 @@ describe('Dashboard landing page actions header tests', () => {
const { getByText } = render(
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
<DashboardProvider>
<DashboardProvider dashboardId="4">
<DashboardDescription
handle={{
active: false,
@@ -187,26 +182,19 @@ describe('Dashboard landing page actions header tests', () => {
(useLocation as jest.Mock).mockReturnValue(mockLocation);
const mockContextValue: IDashboardContext = {
isDashboardSliderOpen: false,
isDashboardLocked: false,
handleToggleDashboardSlider: jest.fn(),
handleDashboardLockToggle: jest.fn(),
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,
dashboardId: '4',
layouts: [],
panelMap: {},
setPanelMap: jest.fn(),
setLayouts: jest.fn(),
setSelectedDashboard: jest.fn(),
updatedTimeRef: { current: null },
toScrollWidgetId: '',
setToScrollWidgetId: jest.fn(),
updateLocalStorageDashboardVariables: jest.fn(),
dashboardQueryRangeCalled: false,
setDashboardQueryRangeCalled: jest.fn(),
selectedRowWidgetId: null,
setSelectedRowWidgetId: jest.fn(),
isDashboardFetching: false,
columnWidths: {},
setColumnWidths: jest.fn(),

View File

@@ -40,6 +40,7 @@ import {
} from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { sortLayout } from 'providers/Dashboard/util';
import { DashboardData } from 'types/api/dashboard/getAll';
import { Props } from 'types/api/dashboard/update';
@@ -48,10 +49,10 @@ import { ComponentTypes } from 'utils/permission';
import { v4 as uuid } from 'uuid';
import DashboardHeader from '../components/DashboardHeader/DashboardHeader';
import DashboardGraphSlider from '../ComponentsSlider';
import DashboardSettings from '../DashboardSettings';
import { Base64Icons } from '../DashboardSettings/General/utils';
import DashboardVariableSelection from '../DashboardVariablesSelection';
import PanelTypeSelectionModal from '../PanelTypeSelectionModal';
import SettingsDrawer from './SettingsDrawer';
import { VariablesSettingsTab } from './types';
import {
@@ -69,6 +70,9 @@ interface DashboardDescriptionProps {
// eslint-disable-next-line sonarjs/cognitive-complexity
function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const { handle } = props;
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const {
selectedDashboard,
panelMap,
@@ -77,8 +81,6 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
setLayouts,
isDashboardLocked,
setSelectedDashboard,
handleToggleDashboardSlider,
setSelectedRowWidgetId,
handleDashboardLockToggle,
} = useDashboard();
@@ -146,15 +148,14 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true);
setIsPanelTypeSelectionModalOpen(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleToggleDashboardSlider]);
}, [setIsPanelTypeSelectionModalOpen]);
const handleLockDashboardToggle = (): void => {
setIsDashbordSettingsOpen(false);
@@ -523,7 +524,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
<DashboardVariableSelection />
</section>
)}
<DashboardGraphSlider />
<PanelTypeSelectionModal />
<Modal
open={isRenameDashboardOpen}

View File

@@ -9,7 +9,6 @@ import {
} from 'hooks/dashboard/useDashboardVariables';
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { initializeDefaultVariables } from 'providers/Dashboard/initializeDefaultVariables';
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
import {
enqueueDescendantsOfVariable,
@@ -30,7 +29,7 @@ function DashboardVariableSelection(): JSX.Element | null {
updateLocalStorageDashboardVariables,
} = useDashboard();
const { updateUrlVariable, getUrlVariables } = useVariablesFromUrl();
const { updateUrlVariable } = useVariablesFromUrl();
const { dashboardVariables } = useDashboardVariables();
const dashboardId = useDashboardVariablesSelector(
@@ -50,15 +49,6 @@ function DashboardVariableSelection(): JSX.Element | null {
(state) => state.globalTime,
);
useEffect(() => {
// Initialize variables with default values if not in URL
initializeDefaultVariables(
dashboardVariables,
getUrlVariables,
updateUrlVariable,
);
}, [getUrlVariables, updateUrlVariable, dashboardVariables]);
// Memoize the order key to avoid unnecessary triggers
const variableOrderKey = useMemo(() => {
const queryVariableOrderKey = dependencyData?.order?.join(',') ?? '';

View File

@@ -67,17 +67,18 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
const oldVariables = prev?.data.variables;
// this is added to handle case where we have two different
// schemas for variable response
if (oldVariables?.[id]) {
oldVariables[id] = {
...oldVariables[id],
const updatedVariables = { ...oldVariables };
if (updatedVariables?.[id]) {
updatedVariables[id] = {
...updatedVariables[id],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
};
}
if (oldVariables?.[name]) {
oldVariables[name] = {
...oldVariables[name],
if (updatedVariables?.[name]) {
updatedVariables[name] = {
...updatedVariables[name],
selectedValue: value,
allSelected,
haveCustomValuesSelected,
@@ -87,9 +88,7 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
...prev,
data: {
...prev?.data,
variables: {
...oldVariables,
},
variables: updatedVariables,
},
};
}

View File

@@ -1,4 +1,4 @@
.graph-selection {
.panel-type-selection-modal {
.ant-modal-content {
width: 515px;
max-height: 646px;
@@ -76,6 +76,11 @@
content: none;
}
}
.panel-type-text {
text-align: center;
margin-top: 1rem;
}
}
}
@@ -114,7 +119,7 @@
}
.lightMode {
.graph-selection {
.panel-type-selection-modal {
.ant-modal-content {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);

View File

@@ -0,0 +1,68 @@
import { memo } from 'react';
import { Card, Modal, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES, PANEL_TYPES_INITIAL_QUERY } from 'constants/queryBuilder';
import createQueryParams from 'lib/createQueryParams';
import history from 'lib/history';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { v4 as uuid } from 'uuid';
import { PanelTypesWithData } from './menuItems';
import './PanelTypeSelectionModal.styles.scss';
function PanelTypeSelectionModal(): JSX.Element {
const {
isPanelTypeSelectionModalOpen,
setIsPanelTypeSelectionModalOpen,
} = usePanelTypeSelectionModalStore();
const onClickHandler = (name: PANEL_TYPES) => (): void => {
const id = uuid();
setIsPanelTypeSelectionModalOpen(false);
logEvent('Dashboard Detail: New panel type selected', {
panelType: name,
widgetId: id,
});
const queryParams = {
graphType: name,
widgetId: id,
[QueryParams.compositeQuery]: JSON.stringify(
PANEL_TYPES_INITIAL_QUERY[name],
),
};
history.push(
`${history.location.pathname}/new?${createQueryParams(queryParams)}`,
);
};
const handleCardClick = (panelType: PANEL_TYPES): void => {
onClickHandler(panelType)();
};
return (
<Modal
open={isPanelTypeSelectionModalOpen}
onCancel={(): void => {
setIsPanelTypeSelectionModalOpen(false);
}}
rootClassName="panel-type-selection-modal"
footer={null}
title="New Panel"
>
<div className="panel-selection">
{PanelTypesWithData.map(({ name, icon, display }) => (
<Card onClick={(): void => handleCardClick(name)} id={name} key={name}>
{icon}
<Typography className="panel-type-text">{display}</Typography>
</Card>
))}
</div>
</Modal>
);
}
export default memo(PanelTypeSelectionModal);

View File

@@ -9,7 +9,7 @@ import {
Table,
} from 'lucide-react';
const Items: ItemsProps[] = [
export const PanelTypesWithData: ItemsProps[] = [
{
name: PANEL_TYPES.TIME_SERIES,
icon: <LineChart size={16} color={Color.BG_ROBIN_400} />,
@@ -52,5 +52,3 @@ export interface ItemsProps {
icon: JSX.Element;
display: string;
}
export default Items;

View File

@@ -1,9 +1,9 @@
import { renderHook } from '@testing-library/react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
import { useScrollWidgetIntoView } from '../useScrollWidgetIntoView';
jest.mock('providers/Dashboard/Dashboard');
jest.mock('providers/Dashboard/helpers/scrollToWidgetIdHelper');
type MockHTMLElement = {
scrollIntoView: jest.Mock;
@@ -18,25 +18,35 @@ function createMockElement(): MockHTMLElement {
}
describe('useScrollWidgetIntoView', () => {
const mockedUseDashboard = useDashboard as jest.MockedFunction<
typeof useDashboard
const mockedUseScrollToWidgetIdStore = useScrollToWidgetIdStore as jest.MockedFunction<
typeof useScrollToWidgetIdStore
>;
let mockElement: MockHTMLElement;
let ref: React.RefObject<HTMLDivElement>;
let setToScrollWidgetId: jest.Mock;
function mockStore(toScrollWidgetId: string): void {
const storeState = { toScrollWidgetId, setToScrollWidgetId };
mockedUseScrollToWidgetIdStore.mockImplementation(
(selector) =>
selector(
(storeState as unknown) as Parameters<typeof selector>[0],
) as ReturnType<typeof useScrollToWidgetIdStore>,
);
}
beforeEach(() => {
jest.clearAllMocks();
mockElement = createMockElement();
ref = ({
current: mockElement,
} as unknown) as React.RefObject<HTMLDivElement>;
setToScrollWidgetId = jest.fn();
});
it('scrolls into view and focuses when toScrollWidgetId matches widget id', () => {
const setToScrollWidgetId = jest.fn();
const mockElement = createMockElement();
const ref = ({
current: mockElement,
} as unknown) as React.RefObject<HTMLDivElement>;
mockedUseDashboard.mockReturnValue(({
toScrollWidgetId: 'widget-id',
setToScrollWidgetId,
} as unknown) as ReturnType<typeof useDashboard>);
mockStore('widget-id');
renderHook(() => useScrollWidgetIntoView('widget-id', ref));
@@ -49,16 +59,7 @@ describe('useScrollWidgetIntoView', () => {
});
it('does nothing when toScrollWidgetId does not match widget id', () => {
const setToScrollWidgetId = jest.fn();
const mockElement = createMockElement();
const ref = ({
current: mockElement,
} as unknown) as React.RefObject<HTMLDivElement>;
mockedUseDashboard.mockReturnValue(({
toScrollWidgetId: 'other-widget',
setToScrollWidgetId,
} as unknown) as ReturnType<typeof useDashboard>);
mockStore('other-widget');
renderHook(() => useScrollWidgetIntoView('widget-id', ref));

View File

@@ -1,5 +1,5 @@
import { RefObject, useEffect } from 'react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
/**
* Scrolls the given widget container into view when the dashboard
@@ -11,7 +11,10 @@ export function useScrollWidgetIntoView<T extends HTMLElement>(
widgetId: string,
widgetContainerRef: RefObject<T>,
): void {
const { toScrollWidgetId, setToScrollWidgetId } = useDashboard();
const toScrollWidgetId = useScrollToWidgetIdStore((s) => s.toScrollWidgetId);
const setToScrollWidgetId = useScrollToWidgetIdStore(
(s) => s.setToScrollWidgetId,
);
useEffect(() => {
if (toScrollWidgetId === widgetId) {

View File

@@ -1,12 +1,10 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useScrollWidgetIntoView } from 'container/DashboardContainer/visualization/hooks/useScrollWidgetIntoView';
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import ContextMenu from 'periscope/components/ContextMenu';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
@@ -35,8 +33,6 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
useScrollWidgetIntoView(widget.id, graphRef);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
@@ -62,7 +58,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
currentQuery: widget.query,
onClick: clickHandlerWithContextMenu,
onDragSelect,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
apiResponse: queryResponse?.data?.payload,
timezone,
panelMode,
minTimeScale: minTimeScale,

View File

@@ -11,7 +11,6 @@ import { get } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryData } from 'types/api/widgets/getQuery';
import { AlignedData } from 'uplot';
import { PanelMode } from '../types';
@@ -44,7 +43,7 @@ export function prepareBarPanelConfig({
currentQuery: Query;
onClick: OnClickPluginOpts['onClick'];
onDragSelect: (startTime: number, endTime: number) => void;
apiResponse: MetricRangePayloadProps;
apiResponse?: MetricRangePayloadProps;
timezone: Timezone;
panelMode: PanelMode;
minTimeScale?: number;
@@ -76,13 +75,17 @@ export function prepareBarPanelConfig({
stepInterval: minStepInterval,
});
if (!(apiResponse && apiResponse?.data?.result)) {
// if no data, return the builder without adding any series
return builder;
}
if (widget.stackedBarChart) {
const seriesCount = (apiResponse?.data?.result?.length ?? 0) + 1; // +1 for 1-based uPlot series indices
const seriesCount = (apiResponse.data.result.length ?? 0) + 1; // +1 for 1-based uPlot series indices
builder.setBands(getInitialStackedBands(seriesCount));
}
const seriesList: QueryData[] = apiResponse?.data?.result || [];
seriesList.forEach((series) => {
apiResponse.data.result.forEach((series) => {
const baseLabelName = getLabelName(
series.metric,
series.queryName || '', // query

View File

@@ -1,12 +1,10 @@
import { useMemo, useRef } from 'react';
import { useScrollWidgetIntoView } from 'container/DashboardContainer/visualization/hooks/useScrollWidgetIntoView';
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import Histogram from '../../charts/Histogram/Histogram';
@@ -33,13 +31,11 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
useScrollWidgetIntoView(widget.id, graphRef);
const config = useMemo(() => {
return prepareHistogramPanelConfig({
widget,
isDarkMode,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
apiResponse: queryResponse?.data?.payload,
panelMode,
});
}, [widget, isDarkMode, queryResponse?.data?.payload, panelMode]);
@@ -49,7 +45,7 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
return [];
}
return prepareHistogramPanelData({
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
apiResponse: queryResponse?.data?.payload,
bucketWidth: widget?.bucketWidth,
bucketCount: widget?.bucketCount,
mergeAllActiveQueries: widget?.mergeAllActiveQueries,

View File

@@ -149,7 +149,7 @@ export function prepareHistogramPanelConfig({
isDarkMode,
}: {
widget: Widgets;
apiResponse: MetricRangePayloadProps;
apiResponse?: MetricRangePayloadProps;
panelMode: PanelMode;
isDarkMode: boolean;
}): UPlotConfigBuilder {
@@ -204,7 +204,7 @@ export function prepareHistogramPanelConfig({
fillColor: '#4E74F8',
isDarkMode,
});
} else {
} else if (apiResponse && apiResponse?.data?.result) {
apiResponse.data.result.forEach((series) => {
const baseLabelName = getLabelName(
series.metric,

View File

@@ -2,14 +2,12 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
import ChartManager from 'container/DashboardContainer/visualization/components/ChartManager/ChartManager';
import { usePanelContextMenu } from 'container/DashboardContainer/visualization/hooks/usePanelContextMenu';
import { useScrollWidgetIntoView } from 'container/DashboardContainer/visualization/hooks/useScrollWidgetIntoView';
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { ContextMenu } from 'periscope/components/ContextMenu';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
@@ -34,8 +32,6 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { timezone } = useTimezone();
useScrollWidgetIntoView(widget.id, graphRef);
useEffect((): void => {
const { startTime, endTime } = getTimeRange(queryResponse);
@@ -68,7 +64,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
currentQuery: widget.query,
onClick: clickHandlerWithContextMenu,
onDragSelect,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
apiResponse: queryResponse?.data?.payload,
timezone,
panelMode,
minTimeScale: minTimeScale,

View File

@@ -68,11 +68,12 @@ export const prepareUPlotConfig = ({
currentQuery: Query;
onClick?: OnClickPluginOpts['onClick'];
onDragSelect: (startTime: number, endTime: number) => void;
apiResponse: MetricRangePayloadProps;
apiResponse?: MetricRangePayloadProps;
timezone: Timezone;
panelMode: PanelMode;
minTimeScale?: number;
maxTimeScale?: number;
// eslint-disable-next-line sonarjs/cognitive-complexity
}): UPlotConfigBuilder => {
const stepIntervals: ExecStats['stepIntervals'] = get(
apiResponse,
@@ -100,7 +101,12 @@ export const prepareUPlotConfig = ({
stepInterval: minStepInterval,
});
apiResponse.data?.result?.forEach((series) => {
if (!(apiResponse && apiResponse.data.result)) {
// if no data, return the builder without adding any series
return builder;
}
apiResponse.data.result.forEach((series) => {
const hasSingleValidPoint = hasSingleVisiblePointForSeries(series);
const baseLabelName = getLabelName(
series.metric,

View File

@@ -18,7 +18,7 @@ import { PanelMode } from '../types';
export interface BaseConfigBuilderProps {
id: string;
thresholds?: ThresholdProps[];
apiResponse: MetricRangePayloadProps;
apiResponse?: MetricRangePayloadProps;
isDarkMode: boolean;
onClick?: OnClickPluginOpts['onClick'];
onDragSelect?: (startTime: number, endTime: number) => void;

View File

@@ -10,6 +10,7 @@ import setRetentionApi from 'api/settings/setRetention';
import setRetentionApiV2 from 'api/settings/setRetentionV2';
import TextToolTip from 'components/TextToolTip';
import CustomDomainSettings from 'container/CustomDomainSettings';
import LicenseKeyRow from 'container/GeneralSettings/LicenseKeyRow/LicenseKeyRow';
import GeneralSettingsCloud from 'container/GeneralSettingsCloud';
import useComponentPermission from 'hooks/useComponentPermission';
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
@@ -81,7 +82,7 @@ function GeneralSettings({
logsTtlValuesPayload,
);
const { user } = useAppContext();
const { user, activeLicense } = useAppContext();
const [setRetentionPermission] = useComponentPermission(
['set_retention_period'],
@@ -680,7 +681,15 @@ function GeneralSettings({
</span>
</div>
{showCustomDomainSettings && <CustomDomainSettings />}
{(showCustomDomainSettings || activeLicense?.key) && (
<div className="custom-domain-card">
{showCustomDomainSettings && <CustomDomainSettings />}
{showCustomDomainSettings && activeLicense?.key && (
<div className="custom-domain-card-divider" />
)}
{activeLicense?.key && <LicenseKeyRow />}
</div>
)}
<div className="retention-controls-container">
<div className="retention-controls-header">

View File

@@ -0,0 +1,65 @@
.license-key-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--padding-2) var(--padding-3);
gap: var(--spacing-5);
&__left {
display: inline-flex;
align-items: center;
gap: 12px;
color: var(--l2-foreground);
svg {
flex-shrink: 0;
}
}
&__label {
color: var(--l2-foreground);
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
letter-spacing: -0.07px;
flex-shrink: 0;
}
&__value {
display: inline-flex;
align-items: stretch;
}
&__code {
display: inline-flex;
align-items: center;
padding: 1px 2px;
border-radius: 2px 0 0 2px;
background: var(--l3-background);
border: 1px solid var(--l2-border);
color: var(--l2-foreground);
font-family: 'SF Mono', 'Fira Code', 'Fira Mono', monospace;
font-size: var(--paragraph-base-400-font-size);
line-height: var(--line-height-20);
white-space: nowrap;
margin-right: -1px;
}
&__copy-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
padding: 1px 2px;
border-radius: 0 2px 2px 0;
background: var(--l3-background);
border: 1px solid var(--l2-border);
color: var(--l2-foreground);
cursor: pointer;
flex-shrink: 0;
height: 24px;
&:hover {
background: var(--l3-background-hover);
}
}
}

View File

@@ -0,0 +1,48 @@
import { useCopyToClipboard } from 'react-use';
import { Button } from '@signozhq/button';
import { Copy, KeyRound } from '@signozhq/icons';
import { toast } from '@signozhq/sonner';
import { useAppContext } from 'providers/App/App';
import { getMaskedKey } from 'utils/maskedKey';
import './LicenseKeyRow.styles.scss';
function LicenseKeyRow(): JSX.Element | null {
const { activeLicense } = useAppContext();
const [, copyToClipboard] = useCopyToClipboard();
if (!activeLicense?.key) {
return null;
}
const handleCopyLicenseKey = (text: string): void => {
copyToClipboard(text);
toast.success('License key copied to clipboard.', { richColors: true });
};
return (
<div className="license-key-row">
<span className="license-key-row__left">
<KeyRound size={14} />
<span className="license-key-row__label">SigNoz License Key</span>
</span>
<span className="license-key-row__value">
<code className="license-key-row__code">
{getMaskedKey(activeLicense.key)}
</code>
<Button
type="button"
size="xs"
aria-label="Copy license key"
data-testid="license-key-row-copy-btn"
className="license-key-row__copy-btn"
onClick={(): void => handleCopyLicenseKey(activeLicense.key)}
>
<Copy size={12} />
</Button>
</span>
</div>
);
}
export default LicenseKeyRow;

View File

@@ -0,0 +1,61 @@
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import LicenseKeyRow from '../LicenseKeyRow';
const mockCopyToClipboard = jest.fn();
jest.mock('react-use', () => ({
__esModule: true,
useCopyToClipboard: (): [unknown, jest.Mock] => [null, mockCopyToClipboard],
}));
const mockToastSuccess = jest.fn();
jest.mock('@signozhq/sonner', () => ({
toast: {
success: (...args: unknown[]): unknown => mockToastSuccess(...args),
},
}));
describe('LicenseKeyRow', () => {
afterEach(() => {
jest.clearAllMocks();
});
it('renders nothing when activeLicense key is absent', () => {
const { container } = render(<LicenseKeyRow />, undefined, {
appContextOverrides: { activeLicense: null },
});
expect(container).toBeEmptyDOMElement();
});
it('renders label and masked key when activeLicense key exists', () => {
render(<LicenseKeyRow />, undefined, {
appContextOverrides: {
activeLicense: { key: 'abcdefghij' } as any,
},
});
expect(screen.getByText('SigNoz License Key')).toBeInTheDocument();
expect(screen.getByText('ab·······ij')).toBeInTheDocument();
});
it('calls copyToClipboard and shows success toast when clipboard is available', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<LicenseKeyRow />);
await user.click(screen.getByRole('button', { name: /copy license key/i }));
await waitFor(() => {
expect(mockCopyToClipboard).toHaveBeenCalledWith('test-key');
expect(mockToastSuccess).toHaveBeenCalledWith(
'License key copied to clipboard.',
{
richColors: true,
},
);
});
});
});

View File

@@ -9,18 +9,18 @@ import DashboardSettings from 'container/DashboardContainer/DashboardSettings';
import useComponentPermission from 'hooks/useComponentPermission';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
import './DashboardEmptyState.styles.scss';
export default function DashboardEmptyState(): JSX.Element {
const {
selectedDashboard,
isDashboardLocked,
handleToggleDashboardSlider,
setSelectedRowWidgetId,
} = useDashboard();
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const { selectedDashboard, isDashboardLocked } = useDashboard();
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
@@ -42,15 +42,14 @@ export default function DashboardEmptyState(): JSX.Element {
const [addPanelPermission] = useComponentPermission(permissions, userRole);
const onEmptyWidgetHandler = useCallback(() => {
setSelectedRowWidgetId(null);
handleToggleDashboardSlider(true);
setIsPanelTypeSelectionModalOpen(true);
logEvent('Dashboard Detail: Add new panel clicked', {
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
numberOfPanels: selectedDashboard?.data.widgets?.length,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [handleToggleDashboardSlider]);
}, [setIsPanelTypeSelectionModalOpen]);
const onConfigureClick = useCallback((): void => {
setIsSettingsDrawerOpen(true);

View File

@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { Select, Typography } from 'antd';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GraphTypes from 'container/DashboardContainer/ComponentsSlider/menuItems';
import { PanelTypesWithData } from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
import { handleQueryChange } from 'container/NewWidget/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -59,7 +59,7 @@ function PanelTypeSelector({
data-testid="panel-change-select"
disabled={disabled}
>
{GraphTypes.map((item) => (
{PanelTypesWithData.map((item) => (
<Option key={item.name} value={item.name}>
<div className="view-panel-select-option">
<div className="icon">{item.icon}</div>

View File

@@ -5,6 +5,7 @@ import logEvent from 'api/common/logEvent';
import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V5 } from 'constants/app';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useScrollWidgetIntoView } from 'container/DashboardContainer/visualization/hooks/useScrollWidgetIntoView';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
import { useIsPanelWaitingOnVariable } from 'hooks/dashboard/useVariableFetchState';
@@ -67,11 +68,7 @@ function GridCardGraph({
const [isInternalServerError, setIsInternalServerError] = useState<boolean>(
false,
);
const {
toScrollWidgetId,
setToScrollWidgetId,
setDashboardQueryRangeCalled,
} = useDashboard();
const { setDashboardQueryRangeCalled } = useDashboard();
const {
minTime,
@@ -109,20 +106,11 @@ function GridCardGraph({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const graphRef = useRef<HTMLDivElement>(null);
const widgetContainerRef = useRef<HTMLDivElement>(null);
const isVisible = useIntersectionObserver(graphRef, undefined, true);
const isVisible = useIntersectionObserver(widgetContainerRef, undefined, true);
useEffect(() => {
if (toScrollWidgetId === widget.id) {
graphRef.current?.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
graphRef.current?.focus();
setToScrollWidgetId('');
}
}, [toScrollWidgetId, setToScrollWidgetId, widget.id]);
useScrollWidgetIntoView(widget?.id || '', widgetContainerRef);
const updatedQuery = widget?.query;
@@ -306,7 +294,7 @@ function GridCardGraph({
: headerMenuList;
return (
<div style={{ height: '100%', width: '100%' }} ref={graphRef}>
<div style={{ height: '100%', width: '100%' }} ref={widgetContainerRef}>
{isEmptyLayout ? (
<EmptyWidget />
) : (

View File

@@ -71,7 +71,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
isDashboardLocked,
dashboardQueryRangeCalled,
setDashboardQueryRangeCalled,
setSelectedRowWidgetId,
isDashboardFetching,
columnWidths,
} = useDashboard();
@@ -195,7 +194,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
updateDashboardMutation.mutate(updatedDashboard, {
onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null);
if (updatedDashboard.data) {
if (updatedDashboard.data.data.layout) {
setLayouts(sortLayout(updatedDashboard.data.data.layout));

View File

@@ -5,6 +5,8 @@ import useComponentPermission from 'hooks/useComponentPermission';
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
import { ROLES, USER_ROLES } from 'types/roles';
import { ComponentTypes } from 'utils/permission';
@@ -33,12 +35,11 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
} = props;
const [isRowSettingsOpen, setIsRowSettingsOpen] = useState<boolean>(false);
const {
handleToggleDashboardSlider,
selectedDashboard,
isDashboardLocked,
setSelectedRowWidgetId,
} = useDashboard();
const setIsPanelTypeSelectionModalOpen = usePanelTypeSelectionModalStore(
(s) => s.setIsPanelTypeSelectionModalOpen,
);
const { selectedDashboard, isDashboardLocked } = useDashboard();
const permissions: ComponentTypes[] = ['add_panel'];
const { user } = useAppContext();
@@ -81,8 +82,13 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
disabled={!editWidget && addPanelPermission && !isDashboardLocked}
icon={<Plus size={14} />}
onClick={(): void => {
setSelectedRowWidgetId(id);
handleToggleDashboardSlider(true);
// TODO: @AshwinBhatkal Simplify this check in cleanup of https://github.com/SigNoz/engineering-pod/issues/3953
if (!selectedDashboard?.id) {
return;
}
setSelectedRowWidgetId(selectedDashboard.id, id);
setIsPanelTypeSelectionModalOpen(true);
}}
>
New Panel

View File

@@ -15,6 +15,7 @@ import ROUTES from 'constants/routes';
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history';
import cloneDeep from 'lodash-es/cloneDeep';
import { AnimatePresence } from 'motion/react';
@@ -43,6 +44,7 @@ const homeInterval = 30 * 60 * 1000;
// eslint-disable-next-line sonarjs/cognitive-complexity
export default function Home(): JSX.Element {
const { user } = useAppContext();
const isDarkMode = useIsDarkMode();
const [startTime, setStartTime] = useState<number | null>(null);
const [endTime, setEndTime] = useState<number | null>(null);
@@ -680,7 +682,11 @@ export default function Home(): JSX.Element {
<div className="checklist-img-container">
<img
src="/Images/allInOne.svg"
src={
isDarkMode
? '/Images/allInOne.svg'
: '/Images/allInOneLightMode.svg'
}
alt="checklist-img"
className="checklist-img"
/>

View File

@@ -4,8 +4,8 @@ import { getColorsForSeverityLabels, isRedLike } from '../utils';
describe('getColorsForSeverityLabels', () => {
it('should return slate for blank labels', () => {
expect(getColorsForSeverityLabels('', 0)).toBe(Color.BG_SLATE_300);
expect(getColorsForSeverityLabels(' ', 0)).toBe(Color.BG_SLATE_300);
expect(getColorsForSeverityLabels('', 0)).toBe(Color.BG_VANILLA_400);
expect(getColorsForSeverityLabels(' ', 0)).toBe(Color.BG_VANILLA_400);
});
it('should return correct colors for known severity variants', () => {

View File

@@ -79,7 +79,7 @@ export function getColorsForSeverityLabels(
const trimmed = label.trim();
if (!trimmed) {
return Color.BG_SLATE_300;
return Color.BG_VANILLA_400; // Default color for empty labels
}
const variantColor = SEVERITY_VARIANT_COLORS[trimmed];
@@ -119,6 +119,6 @@ export function getColorsForSeverityLabels(
return (
SAFE_FALLBACK_COLORS[index % SAFE_FALLBACK_COLORS.length] ||
Color.BG_SLATE_400
Color.BG_VANILLA_400
);
}

View File

@@ -39,6 +39,7 @@ import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQuery
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQueryData from 'hooks/useUrlQueryData';
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
import { isEmpty, isUndefined } from 'lodash-es';
import LiveLogs from 'pages/LiveLogs';
import { UpdateTimeInterval } from 'store/actions';
@@ -59,6 +60,7 @@ import LogsActionsContainer from './LogsActionsContainer';
import './LogsExplorerViews.styles.scss';
// eslint-disable-next-line sonarjs/cognitive-complexity
function LogsExplorerViewsContainer({
setIsLoadingQueries,
listQueryKeyRef,
@@ -109,7 +111,7 @@ function LogsExplorerViewsContainer({
const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
const [yAxisUnit, setYAxisUnit] = useState<string>('');
const { yAxisUnit, onUnitChange } = useUrlYAxisUnit('');
const listQuery = useMemo(() => getListQuery(stagedQuery) || null, [
stagedQuery,
@@ -367,10 +369,6 @@ function LogsExplorerViewsContainer({
orderBy,
]);
const onUnitChangeHandler = useCallback((value: string): void => {
setYAxisUnit(value);
}, []);
const chartData = useMemo(() => {
if (!stagedQuery) {
return [];
@@ -488,10 +486,7 @@ function LogsExplorerViewsContainer({
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
<div className="time-series-view-container">
<div className="time-series-view-container-header">
<BuilderUnitsFilter
onChange={onUnitChangeHandler}
yAxisUnit={yAxisUnit}
/>
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
</div>
<TimeSeriesView
isLoading={isLoading || isFetching}

View File

@@ -5,7 +5,6 @@ import NewWidget from 'container/NewWidget';
import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import i18n from 'ReactI18';
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
@@ -104,15 +103,13 @@ describe('LogsPanelComponent', () => {
const renderComponent = async (): Promise<void> => {
render(
<I18nextProvider i18n={i18n}>
<DashboardProvider>
<PreferenceContextProvider>
<NewWidget
selectedGraph={PANEL_TYPES.LIST}
fillSpans={undefined}
yAxisUnit={undefined}
/>
</PreferenceContextProvider>
</DashboardProvider>
<PreferenceContextProvider>
<NewWidget
dashboardId=""
selectedDashboard={undefined}
selectedGraph={PANEL_TYPES.LIST}
/>
</PreferenceContextProvider>
</I18nextProvider>,
);

View File

@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useMemo } from 'react';
import { useQueries } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
@@ -11,6 +11,7 @@ import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsF
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { useErrorModal } from 'providers/ErrorModalProvider';
import { AppState } from 'store/reducers';
@@ -22,14 +23,13 @@ import { GlobalReducer } from 'types/reducer/globalTime';
function TimeSeries(): JSX.Element {
const { stagedQuery, currentQuery } = useQueryBuilder();
const { yAxisUnit, onUnitChange } = useUrlYAxisUnit('');
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const [yAxisUnit, setYAxisUnit] = useState<string>('');
const isValidToConvertToMs = useMemo(() => {
const isValid: boolean[] = [];
@@ -112,10 +112,6 @@ function TimeSeries(): JSX.Element {
[data, isValidToConvertToMs],
);
const onUnitChangeHandler = (value: string): void => {
setYAxisUnit(value);
};
const hasMetricSelected = useMemo(
() => currentQuery.builder.queryData.some((q) => q.aggregateAttribute?.key),
[currentQuery],
@@ -123,7 +119,7 @@ function TimeSeries(): JSX.Element {
return (
<div className="meter-time-series-container">
<BuilderUnitsFilter onChange={onUnitChangeHandler} yAxisUnit={yAxisUnit} />
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
<div className="time-series-container">
{!hasMetricSelected && <EmptyMetricsSearch />}
{hasMetricSelected &&

View File

@@ -4,6 +4,7 @@ import { Typography } from 'antd';
import { useNotifications } from 'hooks/useNotifications';
import { Copy } from 'lucide-react';
import { useAppContext } from 'providers/App/App';
import { getMaskedKey } from 'utils/maskedKey';
import './LicenseSection.styles.scss';
@@ -12,15 +13,6 @@ function LicenseSection(): JSX.Element | null {
const { notifications } = useNotifications();
const [, handleCopyToClipboard] = useCopyToClipboard();
const getMaskedKey = (key: string): string => {
if (!key || key.length < 4) {
return key || 'N/A';
}
return `${key.substring(0, 2)}********${key
.substring(key.length - 2)
.trim()}`;
};
const handleCopyKey = (text: string): void => {
handleCopyToClipboard(text);
notifications.success({

View File

@@ -271,7 +271,7 @@ describe('MySettings Flows', () => {
},
});
expect(within(container).getByText('ab********cd')).toBeInTheDocument();
expect(within(container).getByText('ab·······cd')).toBeInTheDocument();
});
it('Should not mask license key if it is too short', () => {

View File

@@ -8,28 +8,15 @@ import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
import TextToolTip from 'components/TextToolTip';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { QBShortcuts } from 'constants/shortcuts/QBShortcuts';
import {
getDefaultWidgetData,
PANEL_TYPE_TO_QUERY_TYPES,
} from 'container/NewWidget/utils';
import { PANEL_TYPE_TO_QUERY_TYPES } from 'container/NewWidget/utils';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
// import { QueryBuilder } from 'container/QueryBuilder';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { defaultTo, isUndefined } from 'lodash-es';
import { Atom, Terminal } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import {
getNextWidgets,
getPreviousWidgets,
getSelectedWidgetIndex,
} from 'providers/Dashboard/util';
import { Widgets } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import ClickHouseQueryContainer from './QueryBuilder/clickHouse';
@@ -40,77 +27,25 @@ function QuerySection({
selectedGraph,
queryRangeKey,
isLoadingQueries,
selectedWidget,
dashboardVersion,
dashboardId,
dashboardName,
isNewPanel,
}: QueryProps): JSX.Element {
const {
currentQuery,
handleRunQuery: handleRunQueryFromQueryBuilder,
redirectWithQueryBuilderData,
} = useQueryBuilder();
const urlQuery = useUrlQuery();
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
const { selectedDashboard, setSelectedDashboard } = useDashboard();
const isDarkMode = useIsDarkMode();
const { widgets } = selectedDashboard?.data || {};
const getWidget = useCallback(() => {
const widgetId = urlQuery.get('widgetId');
return defaultTo(
widgets?.find((e) => e.id === widgetId),
getDefaultWidgetData(widgetId || '', selectedGraph),
);
}, [urlQuery, widgets, selectedGraph]);
const selectedWidget = getWidget() as Widgets;
const { query } = selectedWidget;
useShareBuilderUrl({ defaultValue: query });
const handleStageQuery = useCallback(
(query: Query): void => {
if (selectedDashboard === undefined) {
return;
}
const selectedWidgetIndex = getSelectedWidgetIndex(
selectedDashboard,
selectedWidget.id,
);
const previousWidgets = getPreviousWidgets(
selectedDashboard,
selectedWidgetIndex,
);
const nextWidgets = getNextWidgets(selectedDashboard, selectedWidgetIndex);
setSelectedDashboard({
...selectedDashboard,
data: {
...selectedDashboard?.data,
widgets: [
...previousWidgets,
{
...selectedWidget,
query,
},
...nextWidgets,
],
},
});
handleRunQueryFromQueryBuilder();
},
[
selectedDashboard,
selectedWidget,
setSelectedDashboard,
handleRunQueryFromQueryBuilder,
],
);
const handleQueryCategoryChange = useCallback(
(qCategory: string): void => {
const currentQueryType = qCategory as EQueryType;
@@ -123,19 +58,16 @@ function QuerySection({
);
const handleRunQuery = (): void => {
const widgetId = urlQuery.get('widgetId');
const isNewPanel = isUndefined(widgets?.find((e) => e.id === widgetId));
logEvent('Panel Edit: Stage and run query', {
dataSource: currentQuery.builder?.queryData?.[0]?.dataSource,
panelType: selectedWidget.panelTypes,
queryType: currentQuery.queryType,
widgetId: selectedWidget.id,
dashboardId: selectedDashboard?.id,
dashboardName: selectedDashboard?.data.title,
dashboardId,
dashboardName,
isNewPanel,
});
handleStageQuery(currentQuery);
handleRunQueryFromQueryBuilder();
};
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
@@ -164,7 +96,7 @@ function QuerySection({
panelType={selectedGraph}
filterConfigs={filterConfigs}
showTraceOperator={selectedGraph !== PANEL_TYPES.LIST}
version={selectedDashboard?.data?.version || 'v3'}
version={dashboardVersion || 'v3'}
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
queryComponents={queryComponents}
signalSourceChangeEnabled
@@ -204,7 +136,7 @@ function QuerySection({
queryComponents,
selectedGraph,
filterConfigs,
selectedDashboard?.data?.version,
dashboardVersion,
isDarkMode,
]);
@@ -261,6 +193,11 @@ interface QueryProps {
selectedGraph: PANEL_TYPES;
queryRangeKey?: QueryKey;
isLoadingQueries?: boolean;
selectedWidget: Widgets;
dashboardVersion?: string;
dashboardId?: string;
dashboardName?: string;
isNewPanel?: boolean;
}
export default QuerySection;

View File

@@ -30,6 +30,8 @@ function LeftContainer({
setRequestData,
setQueryResponse,
enableDrillDown = false,
selectedDashboard,
isNewPanel = false,
}: WidgetGraphProps): JSX.Element {
const { stagedQuery } = useQueryBuilder();
@@ -75,6 +77,11 @@ function LeftContainer({
selectedGraph={selectedGraph}
queryRangeKey={queryRangeKey}
isLoadingQueries={queryResponse.isFetching}
selectedWidget={selectedWidget}
dashboardVersion={ENTITY_VERSION_V5}
dashboardId={selectedDashboard?.id}
dashboardName={selectedDashboard?.data.title}
isNewPanel={isNewPanel}
/>
{selectedGraph === PANEL_TYPES.LIST && (
<ExplorerColumnsRenderer

View File

@@ -1,5 +1,7 @@
.column-unit-selector {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
.heading {
color: var(--bg-vanilla-400);
@@ -30,6 +32,11 @@
width: 100%;
}
}
&-content {
display: flex;
flex-direction: column;
gap: 12px;
}
}
.lightMode {

View File

@@ -72,22 +72,24 @@ export function ColumnUnitSelector(
return (
<section className="column-unit-selector">
<Typography.Text className="heading">Column Units</Typography.Text>
{aggregationQueries.map(({ value, label }) => {
const baseQueryName = value.split('.')[0];
return (
<YAxisUnitSelectorV2
value={columnUnits[value] || ''}
onSelect={(unitValue: string): void =>
handleColumnUnitSelect(value, unitValue)
}
fieldLabel={label}
key={value}
selectedQueryName={baseQueryName}
// Update the column unit value automatically only in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
);
})}
<div className="column-unit-selector-content">
{aggregationQueries.map(({ value, label }) => {
const baseQueryName = value.split('.')[0];
return (
<YAxisUnitSelectorV2
value={columnUnits[value] || ''}
onSelect={(unitValue: string): void =>
handleColumnUnitSelect(value, unitValue)
}
fieldLabel={label}
key={value}
selectedQueryName={baseQueryName}
// Update the column unit value automatically only in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
);
})}
</div>
</section>
);
}

View File

@@ -56,9 +56,6 @@ describe('ContextLinks Component', () => {
/>,
);
// Check that the component renders
expect(screen.getByText('Context Links')).toBeInTheDocument();
// Check that the add button is present
expect(
screen.getByRole('button', { name: /context link/i }),

View File

@@ -14,7 +14,7 @@ import {
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { Button, Modal, Typography } from 'antd';
import { Button, Modal } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { GripVertical, Pencil, Plus, Trash2 } from 'lucide-react';
import {
@@ -134,11 +134,16 @@ function ContextLinks({
return (
<div className="context-links-container">
<Typography.Text className="context-links-text">
Context Links
</Typography.Text>
<div className="context-links-list">
<Button
type="default"
className="add-context-link-button"
icon={<Plus size={12} />}
style={{ width: '100%' }}
onClick={handleAddContextLink}
>
Add Context Link
</Button>
<OverlayScrollbar>
<DndContext
sensors={sensors}
@@ -160,16 +165,6 @@ function ContextLinks({
</SortableContext>
</DndContext>
</OverlayScrollbar>
{/* button to add context link */}
<Button
type="primary"
className="add-context-link-button"
icon={<Plus size={12} />}
onClick={handleAddContextLink}
>
Context Link
</Button>
</div>
<Modal

View File

@@ -2,7 +2,6 @@
display: flex;
flex-direction: column;
gap: 16px;
margin: 12px;
}
.context-links-text {
@@ -110,10 +109,7 @@
}
.add-context-link-button {
display: flex;
align-items: center;
margin: auto;
width: fit-content;
width: 100%;
}
.lightMode {

View File

@@ -1,6 +1,7 @@
.right-container {
display: flex;
flex-direction: column;
padding-bottom: 48px;
.header {
display: flex;
@@ -24,14 +25,14 @@
letter-spacing: -0.07px;
}
}
.name-description {
.control-container {
display: flex;
flex-direction: column;
padding: 12px 12px 16px 12px;
border-top: 1px solid var(--bg-slate-500);
border-bottom: 1px solid var(--bg-slate-500);
gap: 8px;
}
.name-description {
padding: 0 0 4px 0;
.typography {
color: var(--bg-vanilla-400);
@@ -88,9 +89,6 @@
.panel-config {
display: flex;
flex-direction: column;
padding: 12px 12px 16px 12px;
gap: 8px;
border-bottom: 1px solid var(--bg-slate-500);
.typography {
color: var(--bg-vanilla-400);
@@ -104,6 +102,7 @@
}
.panel-type-select {
width: 100%;
.ant-select-selector {
display: flex;
height: 32px;
@@ -137,7 +136,6 @@
}
.fill-gaps {
margin-top: 16px;
display: flex;
padding: 12px;
justify-content: space-between;
@@ -156,31 +154,24 @@
letter-spacing: 0.52px;
text-transform: uppercase;
}
.fill-gaps-text-description {
color: var(--bg-vanilla-400);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
opacity: 0.6;
line-height: 16px; /* 133.333% */
}
}
.log-scale,
.decimal-precision-selector {
margin-top: 16px;
display: flex;
justify-content: space-between;
flex-direction: column;
gap: 8px;
}
.decimal-precision-selector,
.legend-position {
margin-top: 16px;
display: flex;
justify-content: space-between;
flex-direction: column;
gap: 8px;
}
.legend-colors {
margin-top: 16px;
}
.panel-time-text {
margin-top: 16px;
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
@@ -193,7 +184,6 @@
.y-axis-unit-selector,
.y-axis-unit-selector-v2 {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
@@ -278,11 +268,8 @@
}
.stack-chart {
margin-top: 16px;
display: flex;
flex-direction: row;
justify-content: space-between;
gap: 8px;
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
@@ -296,11 +283,6 @@
}
.bucket-config {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
.label {
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
@@ -352,16 +334,13 @@
}
}
.context-links {
border-bottom: 1px solid var(--bg-slate-500);
}
.alerts {
display: flex;
padding: 12px;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--bg-slate-500);
padding: 12px;
min-height: 44px;
border-top: 1px solid var(--bg-slate-500);
cursor: pointer;
.left-section {
@@ -387,6 +366,16 @@
color: var(--bg-vanilla-400);
}
}
.context-links {
padding: 12px 12px 16px 12px;
border-bottom: 1px solid var(--bg-slate-500);
}
.thresholds-section {
padding: 12px 12px 16px 12px;
border-top: 1px solid var(--bg-slate-500);
}
}
.select-option {
@@ -418,9 +407,6 @@
}
.name-description {
border-top: 1px solid var(--bg-vanilla-300);
border-bottom: 1px solid var(--bg-vanilla-300);
.typography {
color: var(--bg-ink-400);
}
@@ -441,8 +427,6 @@
}
.panel-config {
border-bottom: 1px solid var(--bg-vanilla-300);
.typography {
color: var(--bg-ink-400);
}
@@ -478,6 +462,9 @@
.fill-gaps-text {
color: var(--bg-ink-400);
}
.fill-gaps-text-description {
color: var(--bg-ink-400);
}
}
.bucket-config {
@@ -530,7 +517,7 @@
}
.alerts {
border-bottom: 1px solid var(--bg-vanilla-300);
border-top: 1px solid var(--bg-vanilla-300);
.left-section {
.bell-icon {
@@ -549,6 +536,10 @@
.context-links {
border-bottom: 1px solid var(--bg-vanilla-300);
}
.thresholds-section {
border-top: 1px solid var(--bg-vanilla-300);
}
}
.select-option {

View File

@@ -1,5 +1,4 @@
.threshold-selector-container {
padding: 12px;
padding-bottom: 80px;
.threshold-select {

View File

@@ -1,10 +1,10 @@
import { useCallback } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { Typography } from 'antd';
import { Button } from 'antd';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useGetQueryLabels } from 'hooks/useGetQueryLabels';
import { Antenna, Plus } from 'lucide-react';
import { Plus } from 'lucide-react';
import { v4 as uuid } from 'uuid';
import Threshold from './Threshold';
@@ -68,11 +68,14 @@ function ThresholdSelector({
<DndProvider backend={HTML5Backend}>
<div className="threshold-selector-container">
<div className="threshold-select" onClick={addThresholdHandler}>
<div className="left-section">
<Antenna size={14} className="icon" />
<Typography.Text className="text">Thresholds</Typography.Text>
</div>
<Plus size={14} onClick={addThresholdHandler} className="icon" />
<Button
type="default"
icon={<Plus size={14} />}
style={{ width: '100%' }}
onClick={addThresholdHandler}
>
Add Threshold
</Button>
</div>
{thresholds.map((threshold, idx) => (
<Threshold

View File

@@ -8,7 +8,6 @@ import userEvent from '@testing-library/user-event';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { AppContext } from 'providers/App/App';
import { IAppContext } from 'providers/App/types';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import configureStore from 'redux-mock-store';
@@ -96,9 +95,7 @@ const render = (ui: React.ReactElement): ReturnType<typeof rtlRender> =>
<Provider store={createMockStore()}>
<AppContext.Provider value={createMockAppContext() as IAppContext}>
<ErrorModalProvider>
<DashboardProvider>
<QueryBuilderProvider>{ui}</QueryBuilderProvider>
</DashboardProvider>
<QueryBuilderProvider>{ui}</QueryBuilderProvider>
</ErrorModalProvider>
</AppContext.Provider>
</Provider>

View File

@@ -0,0 +1,68 @@
.settings-section {
border-top: 1px solid var(--bg-slate-500);
}
.settings-section-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 12px 12px;
min-height: 44px;
background: transparent;
border: none;
outline: none;
cursor: pointer;
.settings-section-title {
display: flex;
align-items: center;
gap: 8px;
color: var(--bg-vanilla-400);
font-family: 'Space Mono';
font-size: 13px;
font-weight: 400;
text-transform: uppercase;
}
.chevron-icon {
color: var(--bg-vanilla-400);
transition: transform 0.2s ease-in-out;
&.open {
transform: rotate(180deg);
}
}
}
.settings-section-content {
padding: 0 12px 0 12px;
display: flex;
flex-direction: column;
gap: 20px;
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.25s ease, opacity 0.25s ease, padding 0.25s ease;
&.open {
padding-bottom: 24px;
max-height: 1000px;
opacity: 1;
}
}
.lightMode {
.settings-section-header {
.chevron-icon {
color: var(--bg-ink-400);
}
.settings-section-title {
color: var(--bg-ink-400);
}
}
.settings-section {
border-top: 1px solid var(--bg-vanilla-300);
}
}

View File

@@ -0,0 +1,51 @@
import { ReactNode, useState } from 'react';
import { ChevronDown } from 'lucide-react';
import './SettingsSection.styles.scss';
export interface SettingsSectionProps {
title: string;
defaultOpen?: boolean;
children: ReactNode;
icon?: ReactNode;
}
function SettingsSection({
title,
defaultOpen = false,
children,
icon,
}: SettingsSectionProps): JSX.Element {
const [isOpen, setIsOpen] = useState(defaultOpen);
const toggleOpen = (): void => {
setIsOpen((prev) => !prev);
};
return (
<section className="settings-section">
<button
type="button"
className="settings-section-header"
onClick={toggleOpen}
>
<span className="settings-section-title">
{icon ? icon : null} {title}
</span>
<ChevronDown
size={16}
className={isOpen ? 'chevron-icon open' : 'chevron-icon'}
/>
</button>
<div
className={
isOpen ? 'settings-section-content open' : 'settings-section-content'
}
>
{children}
</div>
</section>
);
}
export default SettingsSection;

View File

@@ -14,23 +14,30 @@ import {
Input,
InputNumber,
Select,
Space,
Switch,
Typography,
} from 'antd';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import TimePreference from 'components/TimePreferenceDropDown';
import { PANEL_TYPES, PanelDisplay } from 'constants/queryBuilder';
import GraphTypes, {
import {
ItemsProps,
} from 'container/DashboardContainer/ComponentsSlider/menuItems';
PanelTypesWithData,
} from 'container/DashboardContainer/PanelTypeSelectionModal/menuItems';
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
import useCreateAlerts from 'hooks/queryBuilder/useCreateAlerts';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
Antenna,
Axis3D,
ConciergeBell,
Layers,
LayoutDashboard,
LineChart,
Link,
Pencil,
Plus,
SlidersHorizontal,
Spline,
SquareArrowOutUpRight,
} from 'lucide-react';
@@ -46,6 +53,7 @@ import { DataSource } from 'types/common/queryBuilder';
import { popupContainer } from 'utils/selectPopupContainer';
import { ColumnUnitSelector } from './ColumnUnitSelector/ColumnUnitSelector';
import SettingsSection from './components/SettingsSection/SettingsSection';
import {
panelTypeVsBucketConfig,
panelTypeVsColumnUnitPreferences,
@@ -143,7 +151,7 @@ function RightContainer({
);
const selectedGraphType =
GraphTypes.find((e) => e.name === selectedGraph)?.display || '';
PanelTypesWithData.find((e) => e.name === selectedGraph)?.display || '';
const onCreateAlertsHandler = useCreateAlerts(selectedWidget, 'panelView');
@@ -169,7 +177,7 @@ function RightContainer({
const { currentQuery } = useQueryBuilder();
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(GraphTypes);
const [graphTypes, setGraphTypes] = useState<ItemsProps[]>(PanelTypesWithData);
const dashboardVariableOptions = useMemo<VariableOption[]>(() => {
return Object.entries(dashboardVariables).map(([, value]) => ({
@@ -178,6 +186,21 @@ function RightContainer({
}));
}, [dashboardVariables]);
const isAxisSectionVisible = useMemo(() => allowSoftMinMax || allowLogScale, [
allowSoftMinMax,
allowLogScale,
]);
const isFormattingSectionVisible = useMemo(
() => allowYAxisUnit || allowDecimalPrecision || allowPanelColumnPreference,
[allowYAxisUnit, allowDecimalPrecision, allowPanelColumnPreference],
);
const isLegendSectionVisible = useMemo(
() => allowLegendPosition || allowLegendColors,
[allowLegendPosition, allowLegendColors],
);
const updateCursorAndDropdown = (value: string, pos: number): void => {
setCursorPos(pos);
const lastDollar = value.lastIndexOf('$', pos - 1);
@@ -193,6 +216,15 @@ function RightContainer({
}, 0);
};
const decimapPrecisionOptions = useMemo(() => {
return [
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
];
}, []);
const handleInputCursor = (): void => {
const pos = inputRef.current?.input?.selectionStart ?? 0;
updateCursorAndDropdown(inputValue, pos);
@@ -241,7 +273,7 @@ function RightContainer({
prev.filter((graph) => graph.name !== PANEL_TYPES.LIST),
);
} else {
setGraphTypes(GraphTypes);
setGraphTypes(PanelTypesWithData);
}
}, [currentQuery]);
@@ -263,269 +295,297 @@ function RightContainer({
<div className="right-container">
<section className="header">
<div className="purple-dot" />
<Typography.Text className="header-text">Panel details</Typography.Text>
<Typography.Text className="header-text">Panel Settings</Typography.Text>
</section>
<section className="name-description">
<Typography.Text className="typography">Name</Typography.Text>
<AutoComplete
options={dashboardVariableOptions}
value={inputValue}
onChange={onInputChange}
onSelect={onSelect}
filterOption={filterOption}
style={{ width: '100%' }}
getPopupContainer={popupContainer}
placeholder="Enter the panel name here..."
open={autoCompleteOpen}
>
<Input
rootClassName="name-input"
ref={inputRef}
onSelect={handleInputCursor}
onClick={handleInputCursor}
onBlur={(): void => setAutoCompleteOpen(false)}
/>
</AutoComplete>
<Typography.Text className="typography">Description</Typography.Text>
<TextArea
placeholder="Enter the panel description here..."
bordered
allowClear
value={description}
onChange={(event): void =>
onChangeHandler(setDescription, event.target.value)
}
rootClassName="description-input"
/>
</section>
<section className="panel-config">
<Typography.Text className="typography">Panel Type</Typography.Text>
<Select
onChange={setGraphHandler}
value={selectedGraph}
style={{ width: '100%' }}
className="panel-type-select"
data-testid="panel-change-select"
data-stacking-state={stackedBarChart ? 'true' : 'false'}
>
{graphTypes.map((item) => (
<Option key={item.name} value={item.name}>
<div className="select-option">
<div className="icon">{item.icon}</div>
<Typography.Text className="display">{item.display}</Typography.Text>
</div>
</Option>
))}
</Select>
{allowFillSpans && (
<Space className="fill-gaps">
<Typography className="fill-gaps-text">Fill gaps</Typography>
<Switch
checked={isFillSpans}
size="small"
onChange={(checked): void => setIsFillSpans(checked)}
<SettingsSection title="General" defaultOpen icon={<Pencil size={14} />}>
<section className="name-description control-container">
<Typography.Text className="typography">Name</Typography.Text>
<AutoComplete
options={dashboardVariableOptions}
value={inputValue}
onChange={onInputChange}
onSelect={onSelect}
filterOption={filterOption}
style={{ width: '100%' }}
getPopupContainer={popupContainer}
placeholder="Enter the panel name here..."
open={autoCompleteOpen}
>
<Input
rootClassName="name-input"
ref={inputRef}
onSelect={handleInputCursor}
onClick={handleInputCursor}
onBlur={(): void => setAutoCompleteOpen(false)}
/>
</Space>
)}
{allowPanelTimePreference && (
<>
<Typography.Text className="panel-time-text">
Panel Time Preference
</Typography.Text>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
</>
)}
{allowPanelColumnPreference && (
<ColumnUnitSelector
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
isNewDashboard={isNewDashboard}
/>
)}
{allowYAxisUnit && (
<DashboardYAxisUnitSelectorWrapper
onSelect={setYAxisUnit}
value={yAxisUnit || ''}
fieldLabel={
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE
? 'Unit'
: 'Y Axis Unit'
</AutoComplete>
<Typography.Text className="typography">Description</Typography.Text>
<TextArea
placeholder="Enter the panel description here..."
bordered
allowClear
value={description}
onChange={(event): void =>
onChangeHandler(setDescription, event.target.value)
}
// Only update the y-axis unit value automatically in create mode
shouldUpdateYAxisUnit={isNewDashboard}
rootClassName="description-input"
/>
)}
</section>
</SettingsSection>
{allowDecimalPrecision && (
<section className="decimal-precision-selector">
<Typography.Text className="typography">
Decimal Precision
</Typography.Text>
<section className="panel-config">
<SettingsSection
title="Visualization"
defaultOpen
icon={<LayoutDashboard size={14} />}
>
<section className="panel-type control-container">
<Typography.Text className="typography">Panel Type</Typography.Text>
<Select
options={[
{ label: '0 decimals', value: PrecisionOptionsEnum.ZERO },
{ label: '1 decimal', value: PrecisionOptionsEnum.ONE },
{ label: '2 decimals', value: PrecisionOptionsEnum.TWO },
{ label: '3 decimals', value: PrecisionOptionsEnum.THREE },
{ label: '4 decimals', value: PrecisionOptionsEnum.FOUR },
{ label: 'Full Precision', value: PrecisionOptionsEnum.FULL },
]}
value={decimalPrecision}
style={{ width: '100%' }}
onChange={setGraphHandler}
value={selectedGraph}
className="panel-type-select"
defaultValue={PrecisionOptionsEnum.TWO}
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
/>
data-testid="panel-change-select"
data-stacking-state={stackedBarChart ? 'true' : 'false'}
>
{graphTypes.map((item) => (
<Option key={item.name} value={item.name}>
<div className="select-option">
<div className="icon">{item.icon}</div>
<Typography.Text className="display">{item.display}</Typography.Text>
</div>
</Option>
))}
</Select>
</section>
)}
{allowSoftMinMax && (
<section className="soft-min-max">
<section className="container">
<Typography.Text className="text">Soft Min</Typography.Text>
<InputNumber
type="number"
value={softMin}
onChange={softMinHandler}
rootClassName="input"
{allowPanelTimePreference && (
<section className="panel-time-preference control-container">
<Typography.Text className="panel-time-text">
Panel Time Preference
</Typography.Text>
<TimePreference
{...{
selectedTime,
setSelectedTime,
}}
/>
</section>
<section className="container">
<Typography.Text className="text">Soft Max</Typography.Text>
<InputNumber
value={softMax}
type="number"
rootClassName="input"
onChange={softMaxHandler}
)}
{allowStackingBarChart && (
<section className="stack-chart control-container">
<Typography.Text className="label">Stack series</Typography.Text>
<Switch
checked={stackedBarChart}
size="small"
onChange={(checked): void => setStackedBarChart(checked)}
/>
</section>
</section>
)}
{allowFillSpans && (
<section className="fill-gaps">
<div className="fill-gaps-text-container">
<Typography className="fill-gaps-text">Fill gaps</Typography>
<Typography.Text className="fill-gaps-text-description">
Fill gaps in data with 0 for continuity
</Typography.Text>
</div>
<Switch
checked={isFillSpans}
size="small"
onChange={(checked): void => setIsFillSpans(checked)}
/>
</section>
)}
</SettingsSection>
{isFormattingSectionVisible && (
<SettingsSection
title="Formatting & Units"
icon={<SlidersHorizontal size={14} />}
>
{allowYAxisUnit && (
<DashboardYAxisUnitSelectorWrapper
onSelect={setYAxisUnit}
value={yAxisUnit || ''}
fieldLabel={
selectedGraphType === PanelDisplay.VALUE ||
selectedGraphType === PanelDisplay.PIE
? 'Unit'
: 'Y Axis Unit'
}
// Only update the y-axis unit value automatically in create mode
shouldUpdateYAxisUnit={isNewDashboard}
/>
)}
{allowDecimalPrecision && (
<section className="decimal-precision-selector control-container">
<Typography.Text className="typography">
Decimal Precision
</Typography.Text>
<Select
options={decimapPrecisionOptions}
value={decimalPrecision}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={PrecisionOptionsEnum.TWO}
onChange={(val: PrecisionOption): void => setDecimalPrecision(val)}
/>
</section>
)}
{allowPanelColumnPreference && (
<ColumnUnitSelector
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
isNewDashboard={isNewDashboard}
/>
)}
</SettingsSection>
)}
{allowStackingBarChart && (
<section className="stack-chart">
<Typography.Text className="label">Stack series</Typography.Text>
<Switch
checked={stackedBarChart}
size="small"
onChange={(checked): void => setStackedBarChart(checked)}
/>
</section>
{isAxisSectionVisible && (
<SettingsSection title="Axes" icon={<Axis3D size={14} />}>
{allowSoftMinMax && (
<section className="soft-min-max">
<section className="container">
<Typography.Text className="text">Soft Min</Typography.Text>
<InputNumber
type="number"
value={softMin}
onChange={softMinHandler}
rootClassName="input"
/>
</section>
<section className="container">
<Typography.Text className="text">Soft Max</Typography.Text>
<InputNumber
value={softMax}
type="number"
rootClassName="input"
onChange={softMaxHandler}
/>
</section>
</section>
)}
{allowLogScale && (
<section className="log-scale control-container">
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
<Select
onChange={(value): void =>
setIsLogScale(value === LogScale.LOGARITHMIC)
}
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LogScale.LINEAR}
>
<Option value={LogScale.LINEAR}>
<div className="select-option">
<div className="icon">
<LineChart size={16} />
</div>
<Typography.Text className="display">Linear</Typography.Text>
</div>
</Option>
<Option value={LogScale.LOGARITHMIC}>
<div className="select-option">
<div className="icon">
<Spline size={16} />
</div>
<Typography.Text className="display">Logarithmic</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
</SettingsSection>
)}
{isLegendSectionVisible && (
<SettingsSection title="Legend" icon={<Layers size={14} />}>
{allowLegendPosition && (
<section className="legend-position control-container">
<Typography.Text className="typography">Position</Typography.Text>
<Select
onChange={(value: LegendPosition): void => setLegendPosition(value)}
value={legendPosition}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LegendPosition.BOTTOM}
>
<Option value={LegendPosition.BOTTOM}>
<div className="select-option">
<Typography.Text className="display">Bottom</Typography.Text>
</div>
</Option>
<Option value={LegendPosition.RIGHT}>
<div className="select-option">
<Typography.Text className="display">Right</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendColors && (
<section className="legend-colors">
<LegendColors
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
/>
</section>
)}
</SettingsSection>
)}
{allowBucketConfig && (
<section className="bucket-config">
<Typography.Text className="label">Number of buckets</Typography.Text>
<InputNumber
value={bucketCount || null}
type="number"
min={0}
rootClassName="bucket-input"
placeholder="Default: 30"
onChange={(val): void => {
setBucketCount(val || 0);
}}
/>
<Typography.Text className="label bucket-size-label">
Bucket width
</Typography.Text>
<InputNumber
value={bucketWidth || null}
type="number"
precision={2}
placeholder="Default: Auto"
step={0.1}
min={0.0}
rootClassName="bucket-input"
onChange={(val): void => {
setBucketWidth(val || 0);
}}
/>
<section className="combine-hist">
<Typography.Text className="label">
Merge all series into one
</Typography.Text>
<Switch
checked={combineHistogram}
size="small"
onChange={(checked): void => setCombineHistogram(checked)}
<SettingsSection title="Histogram / Buckets">
<section className="bucket-config control-container">
<Typography.Text className="label">Number of buckets</Typography.Text>
<InputNumber
value={bucketCount || null}
type="number"
min={0}
rootClassName="bucket-input"
placeholder="Default: 30"
onChange={(val): void => {
setBucketCount(val || 0);
}}
/>
<Typography.Text className="label bucket-size-label">
Bucket width
</Typography.Text>
<InputNumber
value={bucketWidth || null}
type="number"
precision={2}
placeholder="Default: Auto"
step={0.1}
min={0.0}
rootClassName="bucket-input"
onChange={(val): void => {
setBucketWidth(val || 0);
}}
/>
<section className="combine-hist">
<Typography.Text className="label">
Merge all series into one
</Typography.Text>
<Switch
checked={combineHistogram}
size="small"
onChange={(checked): void => setCombineHistogram(checked)}
/>
</section>
</section>
</section>
)}
{allowLogScale && (
<section className="log-scale">
<Typography.Text className="typography">Y Axis Scale</Typography.Text>
<Select
onChange={(value): void => setIsLogScale(value === LogScale.LOGARITHMIC)}
value={isLogScale ? LogScale.LOGARITHMIC : LogScale.LINEAR}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LogScale.LINEAR}
>
<Option value={LogScale.LINEAR}>
<div className="select-option">
<div className="icon">
<LineChart size={16} />
</div>
<Typography.Text className="display">Linear</Typography.Text>
</div>
</Option>
<Option value={LogScale.LOGARITHMIC}>
<div className="select-option">
<div className="icon">
<Spline size={16} />
</div>
<Typography.Text className="display">Logarithmic</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendPosition && (
<section className="legend-position">
<Typography.Text className="typography">Legend Position</Typography.Text>
<Select
onChange={(value: LegendPosition): void => setLegendPosition(value)}
value={legendPosition}
style={{ width: '100%' }}
className="panel-type-select"
defaultValue={LegendPosition.BOTTOM}
>
<Option value={LegendPosition.BOTTOM}>
<div className="select-option">
<Typography.Text className="display">Bottom</Typography.Text>
</div>
</Option>
<Option value={LegendPosition.RIGHT}>
<div className="select-option">
<Typography.Text className="display">Right</Typography.Text>
</div>
</Option>
</Select>
</section>
)}
{allowLegendColors && (
<section className="legend-colors">
<LegendColors
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
/>
</section>
</SettingsSection>
)}
</section>
@@ -541,17 +601,25 @@ function RightContainer({
)}
{allowContextLinks && (
<section className="context-links">
<SettingsSection
title="Context Links"
icon={<Link size={14} />}
defaultOpen={!!contextLinks.linksData.length}
>
<ContextLinks
contextLinks={contextLinks}
setContextLinks={setContextLinks}
selectedWidget={selectedWidget}
/>
</section>
</SettingsSection>
)}
{allowThreshold && (
<section>
<SettingsSection
title="Thresholds"
icon={<Antenna size={14} />}
defaultOpen={!!thresholds.length}
>
<ThresholdSelector
thresholds={thresholds}
setThresholds={setThresholds}
@@ -559,7 +627,7 @@ function RightContainer({
selectedGraph={selectedGraph}
columnUnits={columnUnits}
/>
</section>
</SettingsSection>
)}
</div>
);

View File

@@ -36,7 +36,7 @@ const checkStackSeriesState = (
expect(getByTextUtil(container, 'Stack series')).toBeInTheDocument();
const stackSeriesSection = container.querySelector(
'section > .stack-chart',
'.stack-chart',
) as HTMLElement;
expect(stackSeriesSection).toBeInTheDocument();
@@ -310,12 +310,12 @@ describe('Stacking bar in new panel', () => {
const { container, getByText } = render(
<I18nextProvider i18n={i18n}>
<DashboardProvider>
<DashboardProvider dashboardId="">
<PreferenceContextProvider>
<NewWidget
dashboardId=""
selectedDashboard={undefined}
selectedGraph={PANEL_TYPES.BAR}
fillSpans={undefined}
yAxisUnit={undefined}
/>
</PreferenceContextProvider>
</DashboardProvider>
@@ -326,7 +326,7 @@ describe('Stacking bar in new panel', () => {
expect(getByText('Stack series')).toBeInTheDocument();
// Verify section exists
const section = container.querySelector('section > .stack-chart');
const section = container.querySelector('.stack-chart');
expect(section).toBeInTheDocument();
// Verify switch is present and enabled (ant-switch-checked)
@@ -356,11 +356,11 @@ describe('when switching to BAR panel type', () => {
it('should preserve saved stacking value of true', async () => {
const { getByTestId, getByText, container } = render(
<DashboardProvider>
<DashboardProvider dashboardId="">
<NewWidget
dashboardId=""
selectedDashboard={undefined}
selectedGraph={PANEL_TYPES.BAR}
fillSpans={undefined}
yAxisUnit={undefined}
/>
</DashboardProvider>,
);

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { UseQueryResult } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { generatePath, useParams } from 'react-router-dom';
import { generatePath } from 'react-router-dom';
import { WarningOutlined } from '@ant-design/icons';
import { Button, Flex, Modal, Space, Typography } from 'antd';
import logEvent from 'api/common/logEvent';
@@ -32,8 +32,11 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariables';
import { cloneDeep, defaultTo, isEmpty, isUndefined } from 'lodash-es';
import { Check, X } from 'lucide-react';
import { DashboardWidgetPageParams } from 'pages/DashboardWidget';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
import {
clearSelectedRowWidgetId,
getSelectedRowWidgetId,
} from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
import {
getNextWidgets,
getPreviousWidgets,
@@ -78,18 +81,15 @@ import {
import './NewWidget.styles.scss';
function NewWidget({
selectedDashboard,
dashboardId,
selectedGraph,
enableDrillDown = false,
}: NewWidgetProps): JSX.Element {
const { safeNavigate } = useSafeNavigate();
const {
selectedDashboard,
setSelectedDashboard,
setToScrollWidgetId,
selectedRowWidgetId,
setSelectedRowWidgetId,
columnWidths,
} = useDashboard();
const setToScrollWidgetId = useScrollToWidgetIdStore(
(s) => s.setToScrollWidgetId,
);
const { dashboardVariables } = useDashboardVariables();
@@ -134,8 +134,6 @@ function NewWidget({
const query = useUrlQuery();
const { dashboardId } = useParams<DashboardWidgetPageParams>();
const [isNewDashboard, setIsNewDashboard] = useState<boolean>(false);
const logEventCalledRef = useRef(false);
@@ -281,11 +279,10 @@ function NewWidget({
isLogScale,
legendPosition,
customLegendColors,
columnWidths: columnWidths?.[selectedWidget?.id],
columnWidths: selectedWidget.columnWidths,
contextLinks,
};
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
columnUnits,
currentQuery,
@@ -308,8 +305,8 @@ function NewWidget({
isLogScale,
legendPosition,
customLegendColors,
columnWidths,
contextLinks,
selectedWidget.columnWidths,
]);
const closeModal = (): void => {
@@ -442,6 +439,19 @@ function NewWidget({
globalSelectedInterval,
]);
const navigateToDashboardPage = useCallback(() => {
const params = new URLSearchParams();
const urlVariablesQueryString = query.get(QueryParams.variables);
if (urlVariablesQueryString) {
params.set(QueryParams.variables, urlVariablesQueryString);
}
const search = params.toString() ? `?${params.toString()}` : '';
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }) + search);
}, [dashboardId, query, safeNavigate]);
const onClickSaveHandler = useCallback(() => {
if (!selectedDashboard) {
return;
@@ -450,6 +460,8 @@ function NewWidget({
const widgetId = query.get('widgetId') || '';
let updatedLayout = selectedDashboard.data.layout || [];
const selectedRowWidgetId = getSelectedRowWidgetId(dashboardId);
if (isNewDashboard && isEmpty(selectedRowWidgetId)) {
const newLayoutItem = placeWidgetAtBottom(widgetId, updatedLayout);
updatedLayout = [...updatedLayout, newLayoutItem];
@@ -553,20 +565,15 @@ function NewWidget({
};
updateDashboardMutation.mutateAsync(dashboard, {
onSuccess: (updatedDashboard) => {
setSelectedRowWidgetId(null);
setSelectedDashboard(updatedDashboard.data);
onSuccess: () => {
setToScrollWidgetId(selectedWidget?.id || '');
safeNavigate({
pathname: generatePath(ROUTES.DASHBOARD, { dashboardId }),
});
navigateToDashboardPage();
},
});
}, [
selectedDashboard,
query,
isNewDashboard,
selectedRowWidgetId,
afterWidgets,
selectedWidget,
selectedTime.enum,
@@ -575,10 +582,8 @@ function NewWidget({
preWidgets,
updateDashboardMutation,
widgets,
setSelectedDashboard,
setToScrollWidgetId,
setSelectedRowWidgetId,
safeNavigate,
navigateToDashboardPage,
dashboardId,
]);
@@ -587,12 +592,12 @@ function NewWidget({
setDiscardModal(true);
return;
}
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, isQueryModified, safeNavigate]);
navigateToDashboardPage();
}, [isQueryModified, navigateToDashboardPage]);
const discardChanges = useCallback(() => {
safeNavigate(generatePath(ROUTES.DASHBOARD, { dashboardId }));
}, [dashboardId, safeNavigate]);
navigateToDashboardPage();
}, [navigateToDashboardPage]);
const setGraphHandler = (type: PANEL_TYPES): void => {
setIsLoadingPanelData(true);
@@ -626,22 +631,25 @@ function NewWidget({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]);
const onSaveDashboard = useCallback((): void => {
const isNewPanel = useMemo(() => {
const widgetId = query.get('widgetId');
const selectWidget = widgets?.find((e) => e.id === widgetId);
const selectedWidget = widgets?.find((e) => e.id === widgetId);
return isUndefined(selectedWidget);
}, [query, widgets]);
const onSaveDashboard = useCallback((): void => {
logEvent('Panel Edit: Save changes', {
panelType: selectedWidget.panelTypes,
dashboardId: selectedDashboard?.id,
widgetId: selectedWidget.id,
dashboardName: selectedDashboard?.data.title,
queryType: currentQuery.queryType,
isNewPanel: isUndefined(selectWidget),
isNewPanel,
dataSource: currentQuery?.builder?.queryData?.[0]?.dataSource,
});
setSaveModal(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [isNewPanel]);
const isNewTraceLogsAvailable =
currentQuery.queryType === EQueryType.QUERY_BUILDER &&
@@ -681,6 +689,10 @@ function NewWidget({
* on mount here with the currentQuery in the begining itself
*/
setSupersetQuery(currentQuery);
return (): void => {
clearSelectedRowWidgetId(dashboardId);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
@@ -727,12 +739,14 @@ function NewWidget({
}
const widgetId = query.get('widgetId') || '';
const graphType = query.get('graphType') || '';
const variables = query.get(QueryParams.variables) || '';
const queryParams = {
[QueryParams.expandedWidgetId]: widgetId,
[QueryParams.graphType]: graphType,
[QueryParams.compositeQuery]: encodeURIComponent(
JSON.stringify(currentQuery),
),
[QueryParams.variables]: variables,
};
const updatedSearch = createQueryParams(queryParams);
@@ -813,62 +827,62 @@ function NewWidget({
isLoadingPanelData={isLoadingPanelData}
setQueryResponse={setQueryResponse}
enableDrillDown={enableDrillDown}
selectedDashboard={selectedDashboard}
isNewPanel={isNewPanel}
/>
)}
</OverlayScrollbar>
</LeftContainerWrapper>
<RightContainerWrapper>
<OverlayScrollbar>
<RightContainer
setGraphHandler={setGraphHandler}
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
stackedBarChart={stackedBarChart}
setStackedBarChart={setStackedBarChart}
opacity={opacity}
yAxisUnit={yAxisUnit}
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
bucketCount={bucketCount}
bucketWidth={bucketWidth}
combineHistogram={combineHistogram}
setCombineHistogram={setCombineHistogram}
setBucketWidth={setBucketWidth}
setBucketCount={setBucketCount}
setOpacity={setOpacity}
selectedNullZeroValue={selectedNullZeroValue}
setSelectedNullZeroValue={setSelectedNullZeroValue}
selectedGraph={graphType}
setSelectedTime={setSelectedTime}
selectedTime={selectedTime}
setYAxisUnit={setYAxisUnit}
decimalPrecision={decimalPrecision}
setDecimalPrecision={setDecimalPrecision}
thresholds={thresholds}
setThresholds={setThresholds}
selectedWidget={selectedWidget}
isFillSpans={isFillSpans}
setIsFillSpans={setIsFillSpans}
isLogScale={isLogScale}
setIsLogScale={setIsLogScale}
legendPosition={legendPosition}
setLegendPosition={setLegendPosition}
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
softMin={softMin}
setSoftMin={setSoftMin}
softMax={softMax}
setSoftMax={setSoftMax}
contextLinks={contextLinks}
setContextLinks={setContextLinks}
enableDrillDown={enableDrillDown}
isNewDashboard={isNewDashboard}
/>
</OverlayScrollbar>
<RightContainer
setGraphHandler={setGraphHandler}
title={title}
setTitle={setTitle}
description={description}
setDescription={setDescription}
stackedBarChart={stackedBarChart}
setStackedBarChart={setStackedBarChart}
opacity={opacity}
yAxisUnit={yAxisUnit}
columnUnits={columnUnits}
setColumnUnits={setColumnUnits}
bucketCount={bucketCount}
bucketWidth={bucketWidth}
combineHistogram={combineHistogram}
setCombineHistogram={setCombineHistogram}
setBucketWidth={setBucketWidth}
setBucketCount={setBucketCount}
setOpacity={setOpacity}
selectedNullZeroValue={selectedNullZeroValue}
setSelectedNullZeroValue={setSelectedNullZeroValue}
selectedGraph={graphType}
setSelectedTime={setSelectedTime}
selectedTime={selectedTime}
setYAxisUnit={setYAxisUnit}
decimalPrecision={decimalPrecision}
setDecimalPrecision={setDecimalPrecision}
thresholds={thresholds}
setThresholds={setThresholds}
selectedWidget={selectedWidget}
isFillSpans={isFillSpans}
setIsFillSpans={setIsFillSpans}
isLogScale={isLogScale}
setIsLogScale={setIsLogScale}
legendPosition={legendPosition}
setLegendPosition={setLegendPosition}
customLegendColors={customLegendColors}
setCustomLegendColors={setCustomLegendColors}
queryResponse={queryResponse}
softMin={softMin}
setSoftMin={setSoftMin}
softMax={softMax}
setSoftMax={setSoftMax}
contextLinks={contextLinks}
setContextLinks={setContextLinks}
enableDrillDown={enableDrillDown}
isNewDashboard={isNewDashboard}
/>
</RightContainerWrapper>
</PanelContainer>
<Modal

View File

@@ -15,7 +15,14 @@ export const RightContainerWrapper = styled(Col)`
overflow-y: auto;
}
&::-webkit-scrollbar {
width: 0rem;
width: 0.3rem;
}
&::-webkit-scrollbar-thumb {
background: rgb(136, 136, 136);
border-radius: 0.625rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
`;

Some files were not shown because too many files have changed in this diff Show More