Compare commits

..

25 Commits

Author SHA1 Message Date
Shubham Dubey
730986b719 chore(onboardingv2/config): python traces changes
Signed-off-by: Shubham Dubey <shubham@signoz.io>
2025-12-16 19:08:06 +05:30
Nikhil Mantri
497972f23c chore(metrics-explorer): address follow-up comments (#9730) 2025-12-15 14:59:30 +05:30
swapnil-signoz
a9e30919d1 Refactor/aws api gateway dashboard (#9763)
* refactor: updating api gateway dashboard to support multiple types of APIs i.e. REST, HTTP and Websocket.
2025-12-15 14:34:39 +05:30
Ishan
925c4c4a3d fix: UI/UX fixes on Global Actions (CMD / CTRL + K) (#9739)
* feat: command K palette , removed kbar

* chore: updated cmdk for login checks and icons

* feat: updated icons and test cases

* adding more llm monitoring sources to onbaording(frontend) (#9623)

* feat: code update, PR comment fix, package.json update

* feat: code update, removed expand icon, moved keyboard func

* feat: css variable update

* feat: removed kbar from applayout

* feat: updated cursor bot comments

* feat: updated cursor bot and test case file

* feat: scss formatted

* feat: deleted unwanted merge change

---------

Co-authored-by: gkarthi-signoz <goutham@signoz.io>
Co-authored-by: Aditya Singh <adityasinghssj1@gmail.com>
2025-12-15 08:41:00 +05:30
Piyush Singariya
e66bfe5961 feat(JSON): JSON Body Metadata (#9593)
* feat: json Body Keys

* feat: telemetry types

* feat: change ExtractBodyPaths

* chore: minor comment change

* chore: func rename, file rename

* chore: change table names

* chore: reflect changes from the overhaul

* test: fixing test 1

* fix: test TestQueryToKeys

* fix: test TestPrepareLogsQuery

* chore: remove db

* chore: go mod

* chore: changes based on review

* chore: changes based on review

* fix: in LIKE operation

* chore: addressed few changes

* revert: test file

* fix: comparison fix

* test: add TestBuildListLogsJSONIndexesQuery

* fix: in test TestBuildListLogsJSONIndexesQuery

* fix: pull promoted paths in single db call

* fix: reducing db calls

* test: fix TestBuildListLogsJSONIndexesQuery

* fix: test TestConditionForJSONBodySearch

* fix: lint try 1

* chore: review changes based on cursor

* fix: use enums only

---------

Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
Co-authored-by: Nityananda Gohain <nityanandagohain@gmail.com>
2025-12-09 20:47:26 +07:00
Nikhil Mantri
42943f72b7 chore(metric-metadata): do not delete rows, keep inserting, pick latest 2025-12-09 05:46:48 +00:00
Nikhil Mantri
7a72a209e5 chore: metric highlights API for detailed view (#9679) 2025-12-09 00:09:23 +05:30
Karan Balani
44f00943a8 fix: tokenizer cache ttls and guardrails for config (#9776) 2025-12-05 11:01:05 +05:30
Nikhil Mantri
8867e1ef38 chore: metric Metadata Seperate Attributes API (#9622) 2025-12-04 19:38:37 +00:00
Abhi kumar
c08e520941 chore: removed alertRuleProvider from global state (#9648) 2025-12-04 10:27:50 +00:00
Ishan
139cc4452d style: metrics custom function css overflow issues (#9660)
* style: metrics custom function css overflow issues

* feat: add support for recovery threshold (#9428)

* feat: created common component for overflowing input tooltip

* feat: updated function.tsx to have useMemo for debounce

* feat: removed unwanted useEffect and moved inline css to separate file

* feat: re-applied useEffect due to css issues

* feat: removed inline styling

* feat: updated mirror ref to be in common component along with css updates

* feat: reverted prom_rule.go

* feat: code cleanup - input ref/forwardref cleanup

* feat: code cleanup - updated test file

* feat: extracted mirror-ref outside of tooltip

* feat: removed unwanted css

* feat: code optmized

* feat: test file updated

* feat: snapshot update

---------

Co-authored-by: Ishan Uniyal <ishan@Ishans-MacBook-Pro.local>
Co-authored-by: Abhishek Kumar Singh <hritik6058@gmail.com>
2025-12-04 15:04:39 +05:30
Nityananda Gohain
2f3baeb302 fix: fix third party api filtering (#9770) 2025-12-03 23:36:05 +05:30
Abhishek Kumar Singh
3d42b0058e chore: Query filter extraction API (#9617) 2025-12-03 13:13:32 +00:00
Srikanth Chekuri
ed70e3c5f5 chore: update .github/CODEOWNERS (#9759) 2025-12-03 11:06:30 +00:00
Ishan
7d6918f8b6 style: updated subdomain already exists UI error on 409 (#9661)
* style: updated subdomain already exists UI error on 409

* style: updated subdomain default message

---------

Co-authored-by: Ishan Uniyal <ishan@Ishans-MacBook-Pro.local>
2025-12-03 15:12:44 +05:30
primus-bot[bot]
2885bc851e chore(release): bump to v0.104.0 (#9765)
Co-authored-by: primus-bot[bot] <171087277+primus-bot[bot]@users.noreply.github.com>
2025-12-03 12:16:07 +05:30
Vishal Sharma
857258f8c3 feat: expand related search keywords (#9728)
* feat: expand related search keywords and add React Native option to onboarding configurations

* feat: enhance onboarding data source configurations
With nested questions for migration and log collection, and add new related logos.

* chore: update React Native doc links to absolute URLs and remove 404

* feat: revert datasource changes
2025-12-03 06:07:57 +00:00
Yunus M
ece5c2b7ad fix: error details container - add padding and improve typography of title and desc (#9753)
* fix: add padding to error details container

* fix: update typography of title and description
2025-12-03 05:47:30 +00:00
Shaheer Kochai
1078f98388 feat: add support for copying individual JSON tree nodes in log details (#9657)
* feat: implement copy functionality for individual JSON tree nodes in log details

* chore: add tests for individual json tree nodes in log details

* test: enhance copy button tests for BodyTitleRenderer

* feat: add support for copying any node in json tree in log details

* test: update BodyTitleRenderer tests to verify copy functionality for JSON tree nodes
2025-12-03 02:50:02 +00:00
Abhi kumar
b4e2326f38 fix: added y-axis unit selector in traces view (#9761) 2025-12-03 00:43:58 +05:30
Abhi kumar
c79b154215 fix: added fix for duplicate y-axis selector in metrics explorer (#9758) 2025-12-02 23:45:48 +05:30
Yunus M
a59c0188cc feat: enable / revoke public access to a dashboard (#9642) 2025-12-02 22:30:24 +05:30
Shaheer Kochai
3df426625a fix: remove isRoot and isEntrypoint from the list of selectable columns in the columns menu in traces explorer and traces list panel in dashboard (#9629)
* fix: hide isRoot and isEntryPoint options from columns options

* test: add tests to ensure isRoot and isEntryPoint are hidden in column options

* refactor: improve the columns exclusion logic + update test
2025-12-02 16:20:04 +00:00
Karan Balani
646f359f33 feat: add openfga instrumentation configuration (#9754) 2025-12-02 15:28:42 +00:00
Vikrant Gupta
81167c6947 fix(dashboard): send public dashboard id on create (#9755) 2025-12-02 19:57:10 +05:30
169 changed files with 12783 additions and 3777 deletions

40
.github/CODEOWNERS vendored
View File

@@ -3,51 +3,11 @@
# that they own. # that they own.
/frontend/ @YounixM @aks07 /frontend/ @YounixM @aks07
/frontend/src/container/MetricsApplication @srikanthccv
/frontend/src/container/NewWidget/RightContainer/types.ts @srikanthccv
# Onboarding # Onboarding
/frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json @makeavish /frontend/src/container/OnboardingV2Container/onboarding-configs/onboarding-config-with-links.json @makeavish
/frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @makeavish /frontend/src/container/OnboardingV2Container/AddDataSource/AddDataSource.tsx @makeavish
# Dashboard, Alert, Metrics, Service Map, Services
/frontend/src/container/ListOfDashboard/ @srikanthccv
/frontend/src/container/NewDashboard/ @srikanthccv
/frontend/src/pages/DashboardsListPage/ @srikanthccv
/frontend/src/pages/DashboardWidget/ @srikanthccv
/frontend/src/pages/NewDashboard/ @srikanthccv
/frontend/src/providers/Dashboard/ @srikanthccv
# Alerts
/frontend/src/container/AlertHistory/ @srikanthccv
/frontend/src/container/AllAlertChannels/ @srikanthccv
/frontend/src/container/AnomalyAlertEvaluationView/ @srikanthccv
/frontend/src/container/CreateAlertChannels/ @srikanthccv
/frontend/src/container/CreateAlertRule/ @srikanthccv
/frontend/src/container/EditAlertChannels/ @srikanthccv
/frontend/src/container/FormAlertChannels/ @srikanthccv
/frontend/src/container/FormAlertRules/ @srikanthccv
/frontend/src/container/ListAlertRules/ @srikanthccv
/frontend/src/container/TriggeredAlerts/ @srikanthccv
/frontend/src/pages/AlertChannelCreate/ @srikanthccv
/frontend/src/pages/AlertDetails/ @srikanthccv
/frontend/src/pages/AlertHistory/ @srikanthccv
/frontend/src/pages/AlertList/ @srikanthccv
/frontend/src/pages/CreateAlert/ @srikanthccv
/frontend/src/providers/Alert.tsx @srikanthccv
# Metrics
/frontend/src/container/MetricsExplorer/ @srikanthccv
/frontend/src/pages/MetricsApplication/ @srikanthccv
/frontend/src/pages/MetricsExplorer/ @srikanthccv
# Services and Service Map
/frontend/src/container/ServiceApplication/ @srikanthccv
/frontend/src/container/ServiceTable/ @srikanthccv
/frontend/src/pages/Services/ @srikanthccv
/frontend/src/pages/ServiceTopLevelOperations/ @srikanthccv
/frontend/src/container/Home/Services/ @srikanthccv
/deploy/ @SigNoz/devops /deploy/ @SigNoz/devops
.github @SigNoz/devops .github @SigNoz/devops

View File

@@ -176,7 +176,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml # - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz: signoz:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz:v0.103.1 image: signoz/signoz:v0.104.0
command: command:
- --config=/root/config/prometheus.yml - --config=/root/config/prometheus.yml
ports: ports:

View File

@@ -117,7 +117,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml # - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz: signoz:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz:v0.103.1 image: signoz/signoz:v0.104.0
command: command:
- --config=/root/config/prometheus.yml - --config=/root/config/prometheus.yml
ports: ports:

View File

@@ -179,7 +179,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml # - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz: signoz:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.103.1} image: signoz/signoz:${VERSION:-v0.104.0}
container_name: signoz container_name: signoz
command: command:
- --config=/root/config/prometheus.yml - --config=/root/config/prometheus.yml

View File

@@ -111,7 +111,7 @@ services:
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml # - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
signoz: signoz:
!!merge <<: *db-depend !!merge <<: *db-depend
image: signoz/signoz:${VERSION:-v0.103.1} image: signoz/signoz:${VERSION:-v0.104.0}
container_name: signoz container_name: signoz
command: command:
- --config=/root/config/prometheus.yml - --config=/root/config/prometheus.yml

View File

@@ -19,6 +19,7 @@ import (
"github.com/SigNoz/signoz/pkg/query-service/interfaces" "github.com/SigNoz/signoz/pkg/query-service/interfaces"
basemodel "github.com/SigNoz/signoz/pkg/query-service/model" basemodel "github.com/SigNoz/signoz/pkg/query-service/model"
rules "github.com/SigNoz/signoz/pkg/query-service/rules" rules "github.com/SigNoz/signoz/pkg/query-service/rules"
"github.com/SigNoz/signoz/pkg/queryparser"
"github.com/SigNoz/signoz/pkg/signoz" "github.com/SigNoz/signoz/pkg/signoz"
"github.com/SigNoz/signoz/pkg/types" "github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/types/authtypes" "github.com/SigNoz/signoz/pkg/types/authtypes"
@@ -60,6 +61,7 @@ func NewAPIHandler(opts APIHandlerOptions, signoz *signoz.SigNoz) (*APIHandler,
FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore), FieldsAPI: fields.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.TelemetryStore),
Signoz: signoz, Signoz: signoz,
QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier, signoz.Analytics), QuerierAPI: querierAPI.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.Querier, signoz.Analytics),
QueryParserAPI: queryparser.NewAPI(signoz.Instrumentation.ToProviderSettings(), signoz.QueryParser),
}) })
if err != nil { if err != nil {

View File

@@ -47,6 +47,8 @@
"@signozhq/button": "0.0.2", "@signozhq/button": "0.0.2",
"@signozhq/calendar": "0.0.0", "@signozhq/calendar": "0.0.0",
"@signozhq/callout": "0.0.2", "@signozhq/callout": "0.0.2",
"@signozhq/checkbox": "0.0.2",
"@signozhq/command": "0.0.0",
"@signozhq/design-tokens": "1.1.4", "@signozhq/design-tokens": "1.1.4",
"@signozhq/input": "0.0.2", "@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0", "@signozhq/popover": "0.0.0",
@@ -103,7 +105,6 @@
"i18next-http-backend": "^1.3.2", "i18next-http-backend": "^1.3.2",
"jest": "^27.5.1", "jest": "^27.5.1",
"js-base64": "^3.7.2", "js-base64": "^3.7.2",
"kbar": "0.1.0-beta.48",
"less": "^4.1.2", "less": "^4.1.2",
"less-loader": "^10.2.0", "less-loader": "^10.2.0",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" fill="none" viewBox="0 0 150 150"><circle cx="75" cy="75" r="70" fill="#f3f4f6"/><g transform="translate(25 25)"><rect width="60" height="55" fill="#fcd34d" rx="6"/><path stroke="#d97706" stroke-linecap="round" stroke-linejoin="round" stroke-width="4" d="m10 40 15-15 15 8 15-23"/><rect width="35" height="55" x="65" fill="#6ee7b7" rx="6"/><rect width="20" height="6" x="73" y="10" fill="#059669" rx="3"/><rect width="15" height="6" x="73" y="22" fill="#059669" opacity=".7" rx="3"/><rect width="18" height="6" x="73" y="34" fill="#059669" opacity=".7" rx="3"/><rect width="100" height="40" y="60" fill="#a5b4fc" rx="6"/><rect width="80" height="8" x="10" y="70" fill="#4f46e5" rx="4"/><rect width="50" height="8" x="20" y="83" fill="#6366f1" rx="4"/></g></svg>

After

Width:  |  Height:  |  Size: 826 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><path fill="#b31aab" d="m33.172 61.48.176 7.325 7.797 4.785-.176-7.324Zm19.031 30.504-.176-7.18-6.84-4.206c-.085-.059-.203-.145-.289-.203l.176 7.207Zm-24.355 9.688L10.012 90.715l-.438-18.367 8.758-3.746-.172-7.352-13.969 5.969c-1.074.46-1.714 1.441-1.687 2.566l.523 22.055c.032 1.125.73 2.25 1.836 2.941l21.383 13.149c.992.605 2.215.78 3.203.46a.8.8 0 0 0 .29-.113l13.124-5.593-7.129-4.383Zm0 0"/><path fill="#d163ce" d="M85.488 61.047c-.031-1.328-.843-2.625-2.125-3.43L57.38 41.672l-.813.344.176 7.726 20.57 12.63.493 20.648 7.86 4.812.433-.172ZM54.383 97.289 30.262 82.47l-.582-24.883 11-4.7-.203-8.562-17.082 7.293c-1.25.547-2.008 1.672-1.977 3l.668 29.18c.031 1.324.844 2.625 2.125 3.402l28.281 17.387c1.164.723 2.563.894 3.754.547.117-.028.234-.086.348-.145l16.703-7.12-8.32-5.102Zm0 0"/><path fill="#e13eaf" d="M122.234 40.633 85.98 18.343c-1.335-.808-2.937-1.038-4.304-.605-.145.028-.262.086-.406.145l-35.383 15.11c-1.426.605-2.297 1.902-2.27 3.429l.903 37.371c.03 1.527.96 2.996 2.445 3.89l36.254 22.262c1.336.805 2.937 1.035 4.277.606.145-.031.262-.09.406-.145l35.383-15.11c1.426-.605 2.297-1.933 2.27-3.433l-.875-37.367c-.028-1.473-.961-2.969-2.446-3.863M85.398 91.64 53.891 72.293l-.79-32.496 30.727-13.121 31.512 19.347.785 32.497Zm0 0"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" fill="none" viewBox="0 0 150 150"><circle cx="75" cy="75" r="70" fill="#f3f4f6"/><g transform="translate(25 40)"><rect width="12" height="12" fill="#059669" opacity=".8" rx="3"/><rect width="80" height="12" x="20" fill="#10b981" rx="3"/><rect width="12" height="12" y="22" fill="#059669" opacity=".8" rx="3"/><rect width="65" height="12" x="20" y="22" fill="#10b981" rx="3"/><rect width="12" height="12" y="44" fill="#059669" opacity=".8" rx="3"/><rect width="75" height="12" x="20" y="44" fill="#10b981" rx="3"/><rect width="12" height="12" y="66" fill="#059669" opacity=".8" rx="3"/><rect width="50" height="12" x="20" y="66" fill="#10b981" rx="3"/></g></svg>

After

Width:  |  Height:  |  Size: 726 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" fill="none" viewBox="0 0 150 150"><circle cx="75" cy="75" r="70" fill="#f3f4f6"/><defs><linearGradient id="a" x1="0" x2="0" y1="0" y2="1"><stop offset="0%" stop-color="#f59e0b" stop-opacity=".5"/><stop offset="100%" stop-color="#f59e0b" stop-opacity=".05"/></linearGradient></defs><g transform="translate(25 35)"><path stroke="#d1d5db" stroke-linecap="round" stroke-width="2" d="M0 80h100M0 80V0"/><path fill="url(#a)" d="M2 78c18 0 28-28 48-23s30-35 48-40v63z"/><path stroke="#d97706" stroke-linecap="round" stroke-width="5" d="M2 78c18 0 28-28 48-23s30-35 48-40"/><circle cx="50" cy="55" r="4" fill="#f59e0b"/><circle cx="98" cy="15" r="4" fill="#f59e0b"/></g></svg>

After

Width:  |  Height:  |  Size: 733 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="150" height="150" fill="none" viewBox="0 0 150 150"><circle cx="75" cy="75" r="70" fill="#f3f4f6"/><rect width="100" height="14" x="25" y="45" fill="#4f46e5" rx="4"/><rect width="60" height="14" x="40" y="68" fill="#6366f1" rx="4"/><rect width="20" height="14" x="105" y="91" fill="#818cf8" rx="4"/><path stroke="#c7d2fe" stroke-width="2" d="M35 59v16m5 0h-5M100 59v39m5 0h-5"/></svg>

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -245,6 +245,14 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
history.replace(newLocation); history.replace(newLocation);
return; return;
} }
// if the current route is public dashboard then don't redirect to login
const isPublicDashboard = currentRoute?.path === ROUTES.PUBLIC_DASHBOARD;
if (isPublicDashboard) {
return;
}
// if the current route // if the current route
if (currentRoute) { if (currentRoute) {
const { isPrivate, key } = currentRoute; const { isPrivate, key } = currentRoute;

View File

@@ -4,7 +4,7 @@ import getLocalStorageApi from 'api/browser/localstorage/get';
import setLocalStorageApi from 'api/browser/localstorage/set'; import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent'; import logEvent from 'api/common/logEvent';
import AppLoading from 'components/AppLoading/AppLoading'; import AppLoading from 'components/AppLoading/AppLoading';
import KBarCommandPalette from 'components/KBarCommandPalette/KBarCommandPalette'; import { CmdKPalette } from 'components/cmdKPalette/cmdKPalette';
import NotFound from 'components/NotFound'; import NotFound from 'components/NotFound';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { FeatureKeys } from 'constants/features'; import { FeatureKeys } from 'constants/features';
@@ -22,12 +22,11 @@ import { StatusCodes } from 'http-status-codes';
import history from 'lib/history'; import history from 'lib/history';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback'; import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import posthog from 'posthog-js'; import posthog from 'posthog-js';
import AlertRuleProvider from 'providers/Alert';
import { useAppContext } from 'providers/App/App'; import { useAppContext } from 'providers/App/App';
import { IUser } from 'providers/App/types'; import { IUser } from 'providers/App/types';
import { CmdKProvider } from 'providers/cmdKProvider';
import { DashboardProvider } from 'providers/Dashboard/Dashboard'; import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { ErrorModalProvider } from 'providers/ErrorModalProvider'; import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { KBarCommandPaletteProvider } from 'providers/KBarCommandPaletteProvider';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider'; import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder'; import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useCallback, useEffect, useState } from 'react'; import { Suspense, useCallback, useEffect, useState } from 'react';
@@ -214,7 +213,10 @@ function App(): JSX.Element {
]); ]);
useEffect(() => { useEffect(() => {
if (pathname === ROUTES.ONBOARDING) { if (
pathname === ROUTES.ONBOARDING ||
pathname.startsWith('/public/dashboard/')
) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
window.Pylon('hideChatBubble'); window.Pylon('hideChatBubble');
@@ -362,16 +364,15 @@ function App(): JSX.Element {
<ConfigProvider theme={themeConfig}> <ConfigProvider theme={themeConfig}>
<Router history={history}> <Router history={history}>
<CompatRouter> <CompatRouter>
<KBarCommandPaletteProvider> <CmdKProvider>
<KBarCommandPalette />
<NotificationProvider> <NotificationProvider>
<ErrorModalProvider> <ErrorModalProvider>
{isLoggedInState && <CmdKPalette userRole={user.role} />}
<PrivateRoute> <PrivateRoute>
<ResourceProvider> <ResourceProvider>
<QueryBuilderProvider> <QueryBuilderProvider>
<DashboardProvider> <DashboardProvider>
<KeyboardHotkeysProvider> <KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout> <AppLayout>
<PreferenceContextProvider> <PreferenceContextProvider>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}> <Suspense fallback={<Spinner size="large" tip="Loading..." />}>
@@ -390,7 +391,6 @@ function App(): JSX.Element {
</Suspense> </Suspense>
</PreferenceContextProvider> </PreferenceContextProvider>
</AppLayout> </AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider> </KeyboardHotkeysProvider>
</DashboardProvider> </DashboardProvider>
</QueryBuilderProvider> </QueryBuilderProvider>
@@ -398,7 +398,7 @@ function App(): JSX.Element {
</PrivateRoute> </PrivateRoute>
</ErrorModalProvider> </ErrorModalProvider>
</NotificationProvider> </NotificationProvider>
</KBarCommandPaletteProvider> </CmdKProvider>
</CompatRouter> </CompatRouter>
</Router> </Router>
</ConfigProvider> </ConfigProvider>

View File

@@ -295,3 +295,10 @@ export const MetricsExplorer = Loadable(
export const ApiMonitoring = Loadable( export const ApiMonitoring = Loadable(
() => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'), () => import(/* webpackChunkName: "ApiMonitoring" */ 'pages/ApiMonitoring'),
); );
export const PublicDashboardPage = Loadable(
() =>
import(
/* webpackChunkName: "Public Dashboard Page" */ 'pages/PublicDashboard'
),
);

View File

@@ -34,6 +34,7 @@ import {
OrgOnboarding, OrgOnboarding,
PasswordReset, PasswordReset,
PipelinePage, PipelinePage,
PublicDashboardPage,
ServiceMapPage, ServiceMapPage,
ServiceMetricsPage, ServiceMetricsPage,
ServicesTablePage, ServicesTablePage,
@@ -169,6 +170,13 @@ const routes: AppRoutes[] = [
isPrivate: true, isPrivate: true,
key: 'DASHBOARD', key: 'DASHBOARD',
}, },
{
path: ROUTES.PUBLIC_DASHBOARD,
exact: false,
component: PublicDashboardPage,
isPrivate: false,
key: 'PUBLIC_DASHBOARD',
},
{ {
path: ROUTES.DASHBOARD_WIDGET, path: ROUTES.DASHBOARD_WIDGET,
exact: true, exact: true,

View File

@@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { CreatePublicDashboardProps } from 'types/api/dashboard/public/create';
const createPublicDashboard = async (
props: CreatePublicDashboardProps,
): Promise<SuccessResponseV2<CreatePublicDashboardProps>> => {
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = '30m' } = props;
try {
const response = await axios.post(
`/dashboards/${dashboardId}/public`,
{ timeRangeEnabled, defaultTimeRange },
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default createPublicDashboard;

View File

@@ -0,0 +1,20 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardDataProps, PayloadProps,PublicDashboardDataProps } from 'types/api/dashboard/public/get';
const getPublicDashboardData = async (props: GetPublicDashboardDataProps): Promise<SuccessResponseV2<PublicDashboardDataProps>> => {
try {
const response = await axios.get<PayloadProps>(`/public/dashboards/${props.id}`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getPublicDashboardData;

View File

@@ -0,0 +1,20 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardMetaProps, PayloadProps,PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
const getPublicDashboardMeta = async (props: GetPublicDashboardMetaProps): Promise<SuccessResponseV2<PublicDashboardMetaProps>> => {
try {
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}/public`);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getPublicDashboardMeta;

View File

@@ -0,0 +1,27 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { MetricRangePayloadV5 } from 'api/v5/v5';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { GetPublicDashboardWidgetDataProps } from 'types/api/dashboard/public/getWidgetData';
const getPublicDashboardWidgetData = async (props: GetPublicDashboardWidgetDataProps): Promise<SuccessResponseV2<MetricRangePayloadV5>> => {
try {
const response = await axios.get(`/public/dashboards/${props.id}/widgets/${props.index}/query_range`, {
params: {
startTime: props.startTime,
endTime: props.endTime,
},
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default getPublicDashboardWidgetData;

View File

@@ -0,0 +1,22 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps,RevokePublicDashboardAccessProps } from 'types/api/dashboard/public/delete';
const revokePublicDashboardAccess = async (
props: RevokePublicDashboardAccessProps,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.delete<PayloadProps>(`/dashboards/${props.id}/public`);
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default revokePublicDashboardAccess;

View File

@@ -0,0 +1,28 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { UpdatePublicDashboardProps } from 'types/api/dashboard/public/update';
const updatePublicDashboard = async (
props: UpdatePublicDashboardProps,
): Promise<SuccessResponseV2<UpdatePublicDashboardProps>> => {
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = '30m' } = props;
try {
const response = await axios.put(
`/dashboards/${dashboardId}/public`,
{ timeRangeEnabled, defaultTimeRange },
);
return {
httpStatusCode: response.status,
data: response.data.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default updatePublicDashboard;

View File

@@ -14,6 +14,8 @@ import '@signozhq/badge';
import '@signozhq/button'; import '@signozhq/button';
import '@signozhq/calendar'; import '@signozhq/calendar';
import '@signozhq/callout'; import '@signozhq/callout';
import '@signozhq/checkbox';
import '@signozhq/command';
import '@signozhq/design-tokens'; import '@signozhq/design-tokens';
import '@signozhq/input'; import '@signozhq/input';
import '@signozhq/popover'; import '@signozhq/popover';

View File

@@ -62,6 +62,8 @@ interface CustomTimePickerProps {
showLiveLogs?: boolean; showLiveLogs?: boolean;
onGoLive?: () => void; onGoLive?: () => void;
onExitLiveLogs?: () => void; onExitLiveLogs?: () => void;
/** When false, hides the "Recently Used" time ranges section */
showRecentlyUsed?: boolean;
} }
function CustomTimePicker({ function CustomTimePicker({
@@ -81,6 +83,7 @@ function CustomTimePicker({
onGoLive, onGoLive,
onExitLiveLogs, onExitLiveLogs,
showLiveLogs, showLiveLogs,
showRecentlyUsed = true,
}: CustomTimePickerProps): JSX.Element { }: CustomTimePickerProps): JSX.Element {
const [ const [
selectedTimePlaceholderValue, selectedTimePlaceholderValue,
@@ -395,6 +398,7 @@ function CustomTimePicker({
setActiveView={setActiveView} setActiveView={setActiveView}
setIsOpenedFromFooter={setIsOpenedFromFooter} setIsOpenedFromFooter={setIsOpenedFromFooter}
isOpenedFromFooter={isOpenedFromFooter} isOpenedFromFooter={isOpenedFromFooter}
showRecentlyUsed={showRecentlyUsed}
/> />
) : ( ) : (
content content
@@ -464,4 +468,5 @@ CustomTimePicker.defaultProps = {
onCustomTimeStatusUpdate: noop, onCustomTimeStatusUpdate: noop,
onExitLiveLogs: noop, onExitLiveLogs: noop,
showLiveLogs: false, showLiveLogs: false,
showRecentlyUsed: true,
}; };

View File

@@ -47,6 +47,7 @@ interface CustomTimePickerPopoverContentProps {
isOpenedFromFooter: boolean; isOpenedFromFooter: boolean;
setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>; setIsOpenedFromFooter: Dispatch<SetStateAction<boolean>>;
onExitLiveLogs: () => void; onExitLiveLogs: () => void;
showRecentlyUsed: boolean;
} }
interface RecentlyUsedDateTimeRange { interface RecentlyUsedDateTimeRange {
@@ -72,6 +73,7 @@ function CustomTimePickerPopoverContent({
isOpenedFromFooter, isOpenedFromFooter,
setIsOpenedFromFooter, setIsOpenedFromFooter,
onExitLiveLogs, onExitLiveLogs,
showRecentlyUsed = true,
}: CustomTimePickerPopoverContentProps): JSX.Element { }: CustomTimePickerPopoverContentProps): JSX.Element {
const { pathname } = useLocation(); const { pathname } = useLocation();
@@ -224,6 +226,7 @@ function CustomTimePickerPopoverContent({
<div>{getTimeChips(RelativeDurationSuggestionOptions)}</div> <div>{getTimeChips(RelativeDurationSuggestionOptions)}</div>
</div> </div>
{showRecentlyUsed && (
<div className="recently-used-container"> <div className="recently-used-container">
<div className="time-heading">RECENTLY USED</div> <div className="time-heading">RECENTLY USED</div>
<div className="recently-used-range"> <div className="recently-used-range">
@@ -251,6 +254,7 @@ function CustomTimePickerPopoverContent({
))} ))}
</div> </div>
</div> </div>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -1,152 +0,0 @@
.kbar-command-palette__positioner {
position: fixed;
inset: 0;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 1rem;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(6px);
z-index: 50;
}
.kbar-command-palette__animator {
width: 100%;
max-width: 600px;
}
.kbar-command-palette__card {
background: var(--bg-ink-500);
color: var(--text-vanilla-100);
border-radius: 3px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
overflow: hidden;
display: flex;
flex-direction: column;
}
.kbar-command-palette__search {
padding: 12px 16px;
font-size: 13px;
border: none;
border-bottom: 1px solid var(--border-ink-200);
color: var(--text-vanilla-100);
outline: none;
background-color: var(--bg-ink-500);
}
.kbar-command-palette__section {
padding: 8px 16px 4px;
font-size: 12px;
font-weight: 600;
color: var(--text-robin-500);
font-family: 'Inter', sans-serif;
text-transform: uppercase;
letter-spacing: 0.02em;
}
.kbar-command-palette__item {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
font-size: 13px;
cursor: pointer;
transition: background 0.15s ease;
}
.kbar-command-palette__item:hover,
.kbar-command-palette__item--active {
background: var(--bg-ink-400);
}
.kbar-command-palette__icon {
flex-shrink: 0;
width: 18px;
height: 18px;
color: #444;
}
.kbar-command-palette__shortcut {
margin-left: auto;
display: flex;
gap: 4px;
}
.kbar-command-palette__key {
padding: 2px 6px;
font-size: 12px;
border-radius: 4px;
background: var(--bg-ink-300);
color: var(--text-vanilla-300);
text-transform: uppercase;
font-family: 'Space Mono', monospace;
}
.kbar-command-palette__results-container {
div {
&::-webkit-scrollbar {
width: 0.3rem;
height: 0.3rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
}
}
.lightMode {
.kbar-command-palette__positioner {
background: rgba(0, 0, 0, 0.5);
}
.kbar-command-palette__card {
background: #fff;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
}
.kbar-command-palette__search {
border-bottom: 1px solid #e5e5e5;
color: var(--text-ink-500);
background-color: var(--bg-vanilla-100);
}
.kbar-command-palette__item {
color: var(--text-ink-500);
}
.kbar-command-palette__item:hover,
.kbar-command-palette__item--active {
background: #f5f5f5;
}
.kbar-command-palette__icon {
color: #444;
}
.kbar-command-palette__key {
background: #eee;
color: #555;
}
.kbar-command-palette__results-container {
div {
&::-webkit-scrollbar-thumb {
background: var(--bg-vanilla-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-vanilla-300);
}
}
}
}

View File

@@ -1,69 +0,0 @@
import './KBarCommandPalette.scss';
import {
KBarAnimator,
KBarPortal,
KBarPositioner,
KBarResults,
KBarSearch,
useMatches,
} from 'kbar';
function Results(): JSX.Element {
const { results } = useMatches();
const renderResults = ({
item,
active,
}: {
item: any;
active: boolean;
}): JSX.Element =>
typeof item === 'string' ? (
<div className="kbar-command-palette__section">{item}</div>
) : (
<div
className={`kbar-command-palette__item ${
active ? 'kbar-command-palette__item--active' : ''
}`}
>
{item.icon}
<span>{item.name}</span>
{item.shortcut?.length ? (
<span className="kbar-command-palette__shortcut">
{item.shortcut.map((sc: string) => (
<kbd key={sc} className="kbar-command-palette__key">
{sc}
</kbd>
))}
</span>
) : null}
</div>
);
return (
<div className="kbar-command-palette__results-container">
<KBarResults items={results} onRender={renderResults} />
</div>
);
}
function KBarCommandPalette(): JSX.Element {
return (
<KBarPortal>
<KBarPositioner className="kbar-command-palette__positioner">
<KBarAnimator className="kbar-command-palette__animator">
<div className="kbar-command-palette__card">
<KBarSearch
className="kbar-command-palette__search"
placeholder="Search or type a command..."
/>
<Results />
</div>
</KBarAnimator>
</KBarPositioner>
</KBarPortal>
);
}
export default KBarCommandPalette;

View File

@@ -0,0 +1,16 @@
.overflow-input {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.overflow-input-mirror {
position: absolute;
visibility: hidden;
white-space: pre;
pointer-events: none;
font: inherit;
letter-spacing: inherit;
height: 0;
overflow: hidden;
}

View File

@@ -0,0 +1,119 @@
import { render, screen, userEvent, waitFor, within } from 'tests/test-utils';
import OverflowInputToolTip from './OverflowInputToolTip';
const TOOLTIP_INNER_SELECTOR = '.ant-tooltip-inner';
// Utility to mock overflow behaviour on inputs / elements.
// Stubs HTMLElement.prototype.clientWidth, scrollWidth and offsetWidth used by component.
function mockOverflow(clientWidth: number, scrollWidth: number): void {
Object.defineProperty(HTMLElement.prototype, 'clientWidth', {
configurable: true,
value: clientWidth,
});
Object.defineProperty(HTMLElement.prototype, 'scrollWidth', {
configurable: true,
value: scrollWidth,
});
// mirror.offsetWidth is used to compute mirrorWidth = offsetWidth + 24.
// Use clientWidth so the mirror measurement aligns with the mocked client width in tests.
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', {
configurable: true,
value: clientWidth,
});
}
function queryTooltipInner(): HTMLElement | null {
// find element that has role="tooltip" (could be the inner itself)
const tooltip = document.querySelector<HTMLElement>('[role="tooltip"]');
if (!tooltip) return document.querySelector(TOOLTIP_INNER_SELECTOR);
// if the role element is already the inner, return it; otherwise return its descendant
if (tooltip.classList.contains('ant-tooltip-inner')) return tooltip;
return (
(tooltip.querySelector(TOOLTIP_INNER_SELECTOR) as HTMLElement) ??
document.querySelector(TOOLTIP_INNER_SELECTOR)
);
}
describe('OverflowInputToolTip', () => {
beforeEach(() => {
jest.restoreAllMocks();
});
test('shows tooltip when content overflows and input is clamped at maxAutoWidth', async () => {
mockOverflow(150, 250); // clientWidth >= maxAutoWidth (150), scrollWidth > clientWidth
render(<OverflowInputToolTip value="Very long overflowing text" />);
await userEvent.hover(screen.getByRole('textbox'));
await waitFor(() => {
expect(queryTooltipInner()).not.toBeNull();
});
const tooltipInner = queryTooltipInner();
if (!tooltipInner) throw new Error('Tooltip inner not found');
expect(
within(tooltipInner).getByText('Very long overflowing text'),
).toBeInTheDocument();
});
test('does NOT show tooltip when content does not overflow', async () => {
mockOverflow(150, 100); // content fits (scrollWidth <= clientWidth)
render(<OverflowInputToolTip value="Short text" />);
await userEvent.hover(screen.getByRole('textbox'));
await waitFor(() => {
expect(queryTooltipInner()).toBeNull();
});
});
test('does NOT show tooltip when content overflows but input is NOT at maxAutoWidth', async () => {
mockOverflow(100, 250); // clientWidth < maxAutoWidth (150), scrollWidth > clientWidth
render(<OverflowInputToolTip value="Long but input not clamped" />);
await userEvent.hover(screen.getByRole('textbox'));
await waitFor(() => {
expect(queryTooltipInner()).toBeNull();
});
});
test('uncontrolled input allows typing', async () => {
render(<OverflowInputToolTip defaultValue="Init" />);
const input = screen.getByRole('textbox') as HTMLInputElement;
await userEvent.type(input, 'ABC');
expect(input).toHaveValue('InitABC');
});
test('disabled input never shows tooltip even if overflowing', async () => {
mockOverflow(150, 300);
render(<OverflowInputToolTip value="Overflowing!" disabled />);
await userEvent.hover(screen.getByRole('textbox'));
await waitFor(() => {
expect(queryTooltipInner()).toBeNull();
});
});
test('renders mirror span and input correctly (structural assertions instead of snapshot)', () => {
const { container } = render(<OverflowInputToolTip value="Snapshot" />);
const mirror = container.querySelector('.overflow-input-mirror');
const input = container.querySelector('input') as HTMLInputElement | null;
expect(mirror).toBeTruthy();
expect(mirror?.textContent).toBe('Snapshot');
expect(input).toBeTruthy();
expect(input?.value).toBe('Snapshot');
// width should be set inline (component calculates width on mount)
expect(input?.getAttribute('style')).toContain('width:');
});
});

View File

@@ -0,0 +1,73 @@
/* eslint-disable react/require-default-props */
/* eslint-disable react/jsx-props-no-spreading */
import './OverflowInputToolTip.scss';
import { Input, InputProps, InputRef, Tooltip } from 'antd';
import cx from 'classnames';
import { useEffect, useRef, useState } from 'react';
export interface OverflowTooltipInputProps extends InputProps {
tooltipPlacement?: 'top' | 'bottom' | 'left' | 'right';
minAutoWidth?: number;
maxAutoWidth?: number;
}
function OverflowInputToolTip({
value,
defaultValue,
onChange,
disabled = false,
tooltipPlacement = 'top',
className,
minAutoWidth = 70,
maxAutoWidth = 150,
...rest
}: OverflowTooltipInputProps): JSX.Element {
const inputRef = useRef<InputRef>(null);
const mirrorRef = useRef<HTMLSpanElement | null>(null);
const [isOverflowing, setIsOverflowing] = useState<boolean>(false);
useEffect(() => {
const input = inputRef.current?.input;
const mirror = mirrorRef.current;
if (!input || !mirror) {
setIsOverflowing(false);
return;
}
mirror.textContent = String(value ?? '') || ' ';
const mirrorWidth = mirror.offsetWidth + 24;
const newWidth = Math.min(maxAutoWidth, Math.max(minAutoWidth, mirrorWidth));
input.style.width = `${newWidth}px`;
// consider clamped when mirrorWidth reaches maxAutoWidth (allow -5px tolerance)
const isClamped = mirrorWidth >= maxAutoWidth - 5;
const overflow = input.scrollWidth > input.clientWidth && isClamped;
setIsOverflowing(overflow);
}, [value, disabled, minAutoWidth, maxAutoWidth]);
const tooltipTitle = !disabled && isOverflowing ? String(value ?? '') : '';
return (
<>
<span ref={mirrorRef} aria-hidden className="overflow-input-mirror" />
<Tooltip title={tooltipTitle} placement={tooltipPlacement}>
<Input
{...rest}
value={value}
defaultValue={defaultValue}
onChange={onChange}
disabled={disabled}
ref={inputRef}
className={cx('overflow-input', className)}
/>
</Tooltip>
</>
);
}
OverflowInputToolTip.displayName = 'OverflowInputToolTip';
export default OverflowInputToolTip;

View File

@@ -0,0 +1,3 @@
import OverflowInputToolTip from './OverflowInputToolTip';
export default OverflowInputToolTip;

View File

@@ -0,0 +1,208 @@
/**
* src/components/cmdKPalette/__test__/cmdkPalette.test.tsx
*/
import '@testing-library/jest-dom/extend-expect';
// ---- Mocks (must run BEFORE importing the component) ----
import ROUTES from 'constants/routes';
import history from 'lib/history';
import { render, screen, userEvent } from 'tests/test-utils';
import { CmdKPalette } from '../cmdKPalette';
const HOME_LABEL = 'Go to Home';
beforeAll(() => {
Object.defineProperty(HTMLElement.prototype, 'scrollIntoView', {
configurable: true,
value: jest.fn(),
});
});
afterAll(() => {
// restore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
delete (HTMLElement.prototype as any).scrollIntoView;
});
// mock history.push / replace / go / location
jest.mock('lib/history', () => {
const location = { pathname: '/', search: '', hash: '' };
const stack: { pathname: string; search: string }[] = [
{ pathname: '/', search: '' },
];
const push = jest.fn((path: string) => {
const [rawPath, rawQuery] = path.split('?');
const pathname = rawPath || '/';
const search = path.includes('?') ? `?${rawQuery || ''}` : '';
location.pathname = pathname;
location.search = search;
stack.push({ pathname, search });
return undefined;
});
const replace = jest.fn((path: string) => {
const [rawPath, rawQuery] = path.split('?');
const pathname = rawPath || '/';
const search = path.includes('?') ? `?${rawQuery || ''}` : '';
location.pathname = pathname;
location.search = search;
if (stack.length > 0) {
stack[stack.length - 1] = { pathname, search };
} else {
stack.push({ pathname, search });
}
return undefined;
});
const listen = jest.fn();
const go = jest.fn((n: number) => {
if (n < 0 && stack.length > 1) {
stack.pop();
}
const top = stack[stack.length - 1] || { pathname: '/', search: '' };
location.pathname = top.pathname;
location.search = top.search;
});
return {
push,
replace,
listen,
go,
location,
__stack: stack,
};
});
// Mock ResizeObserver for Jest/jsdom
class ResizeObserver {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, class-methods-use-this
observe() {}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, class-methods-use-this
unobserve() {}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type, class-methods-use-this
disconnect() {}
}
(global as any).ResizeObserver = ResizeObserver;
// mock cmdK provider hook (open state + setter)
const mockSetOpen = jest.fn();
jest.mock('providers/cmdKProvider', (): unknown => ({
useCmdK: (): {
open: boolean;
setOpen: jest.Mock;
openCmdK: jest.Mock;
closeCmdK: jest.Mock;
} => ({
open: true,
setOpen: mockSetOpen,
openCmdK: jest.fn(),
closeCmdK: jest.fn(),
}),
}));
// mock notifications hook
jest.mock('hooks/useNotifications', (): unknown => ({
useNotifications: (): { notifications: [] } => ({ notifications: [] }),
}));
// mock theme hook
jest.mock('hooks/useDarkMode', (): unknown => ({
useThemeMode: (): {
setAutoSwitch: jest.Mock;
setTheme: jest.Mock;
theme: string;
} => ({
setAutoSwitch: jest.fn(),
setTheme: jest.fn(),
theme: 'dark',
}),
}));
// mock updateUserPreference API and react-query mutation
jest.mock('api/v1/user/preferences/name/update', (): jest.Mock => jest.fn());
jest.mock('react-query', (): unknown => {
const actual = jest.requireActual('react-query');
return {
...actual,
useMutation: (): { mutate: jest.Mock } => ({ mutate: jest.fn() }),
};
});
// mock other side-effecty modules
jest.mock('api/common/logEvent', () => jest.fn());
jest.mock('api/browser/localstorage/set', () => jest.fn());
jest.mock('utils/error', () => ({ showErrorNotification: jest.fn() }));
// ---- Tests ----
describe('CmdKPalette', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('renders navigation and settings groups and items', () => {
render(<CmdKPalette userRole="ADMIN" />);
expect(screen.getByText('Navigation')).toBeInTheDocument();
expect(screen.getByText('Settings')).toBeInTheDocument();
expect(screen.getByText(HOME_LABEL)).toBeInTheDocument();
expect(screen.getByText('Go to Dashboards')).toBeInTheDocument();
expect(screen.getByText('Open Sidebar')).toBeInTheDocument();
expect(screen.getByText('Switch to Dark Mode')).toBeInTheDocument();
});
test('clicking a navigation item calls history.push with correct route', async () => {
render(<CmdKPalette userRole="ADMIN" />);
const homeItem = screen.getByText(HOME_LABEL);
await userEvent.click(homeItem);
expect(history.push).toHaveBeenCalledWith(ROUTES.HOME);
});
test('role-based filtering (basic smoke)', () => {
render(<CmdKPalette userRole="VIEWER" />);
// VIEWER still sees basic navigation items
expect(screen.getByText(HOME_LABEL)).toBeInTheDocument();
});
test('keyboard shortcut opens palette via setOpen', () => {
render(<CmdKPalette userRole="ADMIN" />);
const event = new KeyboardEvent('keydown', { key: 'k', ctrlKey: true });
window.dispatchEvent(event);
expect(mockSetOpen).toHaveBeenCalledWith(true);
});
test('items render with icons when provided', () => {
render(<CmdKPalette userRole="ADMIN" />);
const iconHolders = document.querySelectorAll('.cmd-item-icon');
expect(iconHolders.length).toBeGreaterThan(0);
expect(screen.getByText(HOME_LABEL)).toBeInTheDocument();
});
test('closing the palette via handleInvoke sets open to false', async () => {
render(<CmdKPalette userRole="ADMIN" />);
const dashItem = screen.getByText('Go to Dashboards');
await userEvent.click(dashItem);
// last call from handleInvoke should set open to false
expect(mockSetOpen).toHaveBeenCalledWith(false);
});
});

View File

@@ -0,0 +1,55 @@
/* Overlay stays below content */
[data-slot='dialog-overlay'] {
z-index: 50;
}
/* Dialog content always above overlay */
[data-slot='dialog-content'] {
position: fixed;
z-index: 60;
}
.cmdk-section-heading [cmdk-group-heading] {
text-transform: uppercase;
color: var(--bg-slate-100);
}
/* Hide scrollbar but keep scroll */
.cmdk-list-scroll {
scrollbar-width: none; /* Firefox */
}
.cmdk-list-scroll::-webkit-scrollbar {
display: none; /* Chrome, Safari, Edge */
}
.cmdk-list-scroll {
-webkit-overflow-scrolling: touch;
}
.cmdk-input-wrapper {
margin-left: 8px;
}
.cmdk-item-light:hover {
cursor: pointer;
background-color: var(--bg-vanilla-200) !important;
}
.cmdk-item-light[data-selected='true'] {
background-color: var(--bg-vanilla-300) !important;
color: var(--bg-ink-500);
}
.cmdk-item {
cursor: pointer;
}
[cmdk-item] svg {
width: auto;
height: auto;
}
.cmd-item-icon {
margin-right: 8px;
}

View File

@@ -0,0 +1,336 @@
import './cmdKPalette.scss';
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandShortcut,
} from '@signozhq/command';
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import updateUserPreference from 'api/v1/user/preferences/name/update';
import { AxiosError } from 'axios';
import ROUTES from 'constants/routes';
import { USER_PREFERENCES } from 'constants/userPreferences';
import { useThemeMode } from 'hooks/useDarkMode';
import { THEME_MODE } from 'hooks/useDarkMode/constant';
import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import {
BellDot,
BugIcon,
DraftingCompass,
Expand,
HardDrive,
Home,
LayoutGrid,
ListMinus,
ScrollText,
Settings,
} from 'lucide-react';
import React, { useEffect } from 'react';
import { useMutation } from 'react-query';
import { UserPreference } from 'types/api/preferences/preference';
import { showErrorNotification } from 'utils/error';
import { useAppContext } from '../../providers/App/App';
import { useCmdK } from '../../providers/cmdKProvider';
type CmdAction = {
id: string;
name: string;
shortcut?: string[];
keywords?: string;
section?: string;
icon?: React.ReactNode;
roles?: UserRole[];
perform: () => void;
};
type UserRole = 'ADMIN' | 'EDITOR' | 'AUTHOR' | 'VIEWER';
export function CmdKPalette({
userRole,
}: {
userRole: UserRole;
}): JSX.Element | null {
const { open, setOpen } = useCmdK();
const { updateUserPreferenceInContext } = useAppContext();
const { notifications } = useNotifications();
const { setAutoSwitch, setTheme, theme } = useThemeMode();
const { mutate: updateUserPreferenceMutation } = useMutation(
updateUserPreference,
{
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
},
},
);
// toggle palette with ⌘/Ctrl+K
function handleGlobalCmdK(
e: KeyboardEvent,
setOpen: React.Dispatch<React.SetStateAction<boolean>>,
): void {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
e.preventDefault();
setOpen(true);
}
}
const cmdKEffect = (): void | (() => void) => {
const listener = (e: KeyboardEvent): void => {
handleGlobalCmdK(e, setOpen);
};
window.addEventListener('keydown', listener);
return (): void => {
window.removeEventListener('keydown', listener);
setOpen(false);
};
};
useEffect(cmdKEffect, [setOpen]);
function handleThemeChange(value: string): void {
logEvent('Account Settings: Theme Changed', { theme: value });
if (value === 'auto') {
setAutoSwitch(true);
} else {
setAutoSwitch(false);
setTheme(value);
}
}
function onClickHandler(key: string): void {
history.push(key);
}
function handleOpenSidebar(): void {
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, 'true');
const save = { name: USER_PREFERENCES.SIDENAV_PINNED, value: true };
updateUserPreferenceInContext(save as UserPreference);
updateUserPreferenceMutation({
name: USER_PREFERENCES.SIDENAV_PINNED,
value: true,
});
}
function handleCloseSidebar(): void {
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, 'false');
const save = { name: USER_PREFERENCES.SIDENAV_PINNED, value: false };
updateUserPreferenceInContext(save as UserPreference);
updateUserPreferenceMutation({
name: USER_PREFERENCES.SIDENAV_PINNED,
value: false,
});
}
const actions: CmdAction[] = [
{
id: 'home',
name: 'Go to Home',
shortcut: ['shift + h'],
keywords: 'home',
section: 'Navigation',
icon: <Home size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => onClickHandler(ROUTES.HOME),
},
{
id: 'dashboards',
name: 'Go to Dashboards',
shortcut: ['shift + d'],
keywords: 'dashboards',
section: 'Navigation',
icon: <LayoutGrid size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => onClickHandler(ROUTES.ALL_DASHBOARD),
},
{
id: 'services',
name: 'Go to Services',
shortcut: ['shift + s'],
keywords: 'services monitoring',
section: 'Navigation',
icon: <HardDrive size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => onClickHandler(ROUTES.APPLICATION),
},
{
id: 'traces',
name: 'Go to Traces',
shortcut: ['shift + t'],
keywords: 'traces',
section: 'Navigation',
icon: <DraftingCompass size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => onClickHandler(ROUTES.TRACES_EXPLORER),
},
{
id: 'logs',
name: 'Go to Logs',
shortcut: ['shift + l'],
keywords: 'logs',
section: 'Navigation',
icon: <ScrollText size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => onClickHandler(ROUTES.LOGS),
},
{
id: 'alerts',
name: 'Go to Alerts',
shortcut: ['shift + a'],
keywords: 'alerts',
section: 'Navigation',
icon: <BellDot size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => onClickHandler(ROUTES.LIST_ALL_ALERT),
},
{
id: 'exceptions',
name: 'Go to Exceptions',
shortcut: ['shift + e'],
keywords: 'exceptions errors',
section: 'Navigation',
icon: <BugIcon size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => onClickHandler(ROUTES.ALL_ERROR),
},
{
id: 'messaging-queues',
name: 'Go to Messaging Queues',
shortcut: ['shift + m'],
keywords: 'messaging queues mq',
section: 'Navigation',
icon: <ListMinus size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => onClickHandler(ROUTES.MESSAGING_QUEUES_OVERVIEW),
},
{
id: 'my-settings',
name: 'Go to Account Settings',
keywords: 'account settings',
section: 'Navigation',
icon: <Settings size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => onClickHandler(ROUTES.MY_SETTINGS),
},
// Settings
{
id: 'open-sidebar',
name: 'Open Sidebar',
keywords: 'sidebar navigation menu expand',
section: 'Settings',
icon: <Expand size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => handleOpenSidebar(),
},
{
id: 'collapse-sidebar',
name: 'Collapse Sidebar',
keywords: 'sidebar navigation menu collapse',
section: 'Settings',
icon: <Expand size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => handleCloseSidebar(),
},
{
id: 'dark-mode',
name: 'Switch to Dark Mode',
keywords: 'theme dark mode appearance',
section: 'Settings',
icon: <Expand size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => handleThemeChange(THEME_MODE.DARK),
},
{
id: 'light-mode',
name: 'Switch to Light Mode [Beta]',
keywords: 'theme light mode appearance',
section: 'Settings',
icon: <Expand size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => handleThemeChange(THEME_MODE.LIGHT),
},
{
id: 'system-theme',
name: 'Switch to System Theme',
keywords: 'system theme appearance',
section: 'Settings',
icon: <Expand size={14} />,
roles: ['ADMIN', 'EDITOR', 'AUTHOR', 'VIEWER'],
perform: (): void => handleThemeChange(THEME_MODE.SYSTEM),
},
];
// RBAC filter: show action if no roles set OR current user role is included
const permitted = actions.filter(
(a) => !a.roles || a.roles.includes(userRole),
);
// group permitted actions by section
const grouped: [string, CmdAction[]][] = ((): [string, CmdAction[]][] => {
const map = new Map<string, CmdAction[]>();
permitted.forEach((a) => {
const section = a.section ?? 'Other';
const existing = map.get(section);
if (existing) {
existing.push(a);
} else {
map.set(section, [a]);
}
});
return Array.from(map.entries());
})();
const handleInvoke = (action: CmdAction): void => {
try {
action.perform();
} catch (e) {
console.error('Error invoking action', e);
} finally {
setOpen(false);
}
};
return (
<CommandDialog open={open} onOpenChange={setOpen} position="top" offset={110}>
<CommandInput placeholder="Search…" className="cmdk-input-wrapper" />
<CommandList className="cmdk-list-scroll">
<CommandEmpty>No results</CommandEmpty>
{grouped.map(([section, items]) => (
<CommandGroup
key={section}
heading={section}
className="cmdk-section-heading"
>
{items.map((it) => (
<CommandItem
key={it.id}
onSelect={(): void => handleInvoke(it)}
value={it.name}
className={theme === 'light' ? 'cmdk-item-light' : 'cmdk-item'}
>
<span className="cmd-item-icon">{it.icon}</span>
{it.name}
{it.shortcut && it.shortcut.length > 0 && (
<CommandShortcut>{it.shortcut.join(' • ')}</CommandShortcut>
)}
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</CommandDialog>
);
}

View File

@@ -1,4 +1,7 @@
export const REACT_QUERY_KEY = { export const REACT_QUERY_KEY = {
GET_PUBLIC_DASHBOARD: 'GET_PUBLIC_DASHBOARD',
GET_PUBLIC_DASHBOARD_META: 'GET_PUBLIC_DASHBOARD_META',
GET_PUBLIC_DASHBOARD_WIDGET_DATA: 'GET_PUBLIC_DASHBOARD_WIDGET_DATA',
GET_ALL_LICENCES: 'GET_ALL_LICENCES', GET_ALL_LICENCES: 'GET_ALL_LICENCES',
GET_QUERY_RANGE: 'GET_QUERY_RANGE', GET_QUERY_RANGE: 'GET_QUERY_RANGE',
GET_ALL_DASHBOARDS: 'GET_ALL_DASHBOARDS', GET_ALL_DASHBOARDS: 'GET_ALL_DASHBOARDS',

View File

@@ -81,6 +81,7 @@ const ROUTES = {
METER_EXPLORER: '/meter/explorer', METER_EXPLORER: '/meter/explorer',
METER_EXPLORER_VIEWS: '/meter/explorer/views', METER_EXPLORER_VIEWS: '/meter/explorer/views',
HOME_PAGE: '/', HOME_PAGE: '/',
PUBLIC_DASHBOARD: '/public/dashboard/:dashboardId',
} as const; } as const;
export default ROUTES; export default ROUTES;

View File

@@ -391,6 +391,9 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
const routeKey = useMemo(() => getRouteKey(pathname), [pathname]); const routeKey = useMemo(() => getRouteKey(pathname), [pathname]);
const pageTitle = t(routeKey); const pageTitle = t(routeKey);
const isPublicDashboard = pathname.startsWith('/public/dashboard/');
const renderFullScreen = const renderFullScreen =
pathname === ROUTES.GET_STARTED || pathname === ROUTES.GET_STARTED ||
pathname === ROUTES.ONBOARDING || pathname === ROUTES.ONBOARDING ||
@@ -399,7 +402,8 @@ function AppLayout(props: AppLayoutProps): JSX.Element {
pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING || pathname === ROUTES.GET_STARTED_INFRASTRUCTURE_MONITORING ||
pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT || pathname === ROUTES.GET_STARTED_LOGS_MANAGEMENT ||
pathname === ROUTES.GET_STARTED_AWS_MONITORING || pathname === ROUTES.GET_STARTED_AWS_MONITORING ||
pathname === ROUTES.GET_STARTED_AZURE_MONITORING; pathname === ROUTES.GET_STARTED_AZURE_MONITORING ||
isPublicDashboard;
const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false); const [showTrialExpiryBanner, setShowTrialExpiryBanner] = useState(false);

View File

@@ -266,7 +266,10 @@ export default function CustomDomainSettings(): JSX.Element {
<div className="custom-domain-settings-modal-error"> <div className="custom-domain-settings-modal-error">
{updateDomainError.status === 409 ? ( {updateDomainError.status === 409 ? (
<Alert <Alert
message="Youve already updated the custom domain once today. To make further changes, please contact our support team for assistance." message={
(updateDomainError?.response?.data as { error?: string })?.error ||
'Youve already updated the custom domain once today. To make further changes, please contact our support team for assistance.'
}
type="warning" type="warning"
className="update-limit-reached-error" className="update-limit-reached-error"
/> />

View File

@@ -138,9 +138,9 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
const { formatTimezoneAdjustedTimestamp } = useTimezone(); const { formatTimezoneAdjustedTimestamp } = useTimezone();
return ( return (
<> <div className="error-details-container">
<Typography>{errorDetail.exceptionType}</Typography> <Typography.Title level={4}>{errorDetail.exceptionType}</Typography.Title>
<Typography>{errorDetail.exceptionMessage}</Typography> <Typography.Text>{errorDetail.exceptionMessage}</Typography.Text>
<Divider /> <Divider />
<EventContainer> <EventContainer>
@@ -200,7 +200,7 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
<ResizeTable columns={columns} tableLayout="fixed" dataSource={data} /> <ResizeTable columns={columns} tableLayout="fixed" dataSource={data} />
</Space> </Space>
</EditorContainer> </EditorContainer>
</> </div>
); );
} }

View File

@@ -1,3 +1,7 @@
.error-details-container {
padding: 16px;
}
.error-container { .error-container {
height: 50vh; height: 50vh;
} }

View File

@@ -304,14 +304,19 @@ function WidgetHeader({
data-testid="widget-header-search" data-testid="widget-header-search"
/> />
)} )}
{menu && Array.isArray(menu.items) && menu.items.length > 0 && (
<Dropdown menu={menu} trigger={['hover']} placement="bottomRight"> <Dropdown menu={menu} trigger={['hover']} placement="bottomRight">
<MoreOutlined <MoreOutlined
data-testid="widget-header-options" data-testid="widget-header-options"
className={`widget-header-more-options ${ className={`widget-header-more-options ${
parentHover ? 'widget-header-hover' : '' parentHover ? 'widget-header-hover' : ''
} ${globalSearchAvailable ? 'widget-header-more-options-visible' : ''}`} } ${
globalSearchAvailable ? 'widget-header-more-options-visible' : ''
}`}
/> />
</Dropdown> </Dropdown>
)}
</div> </div>
</> </>
)} )}

View File

@@ -7,6 +7,9 @@ import {
QUERY_BUILDER_FUNCTIONS, QUERY_BUILDER_FUNCTIONS,
} from 'constants/antlrQueryConstants'; } from 'constants/antlrQueryConstants';
import { useActiveLog } from 'hooks/logs/useActiveLog'; import { useActiveLog } from 'hooks/logs/useActiveLog';
import { useNotifications } from 'hooks/useNotifications';
import { useCallback } from 'react';
import { useCopyToClipboard } from 'react-use';
import { TitleWrapper } from './BodyTitleRenderer.styles'; import { TitleWrapper } from './BodyTitleRenderer.styles';
import { DROPDOWN_KEY } from './constant'; import { DROPDOWN_KEY } from './constant';
@@ -24,6 +27,8 @@ function BodyTitleRenderer({
value, value,
}: BodyTitleRendererProps): JSX.Element { }: BodyTitleRendererProps): JSX.Element {
const { onAddToQuery } = useActiveLog(); const { onAddToQuery } = useActiveLog();
const [, setCopy] = useCopyToClipboard();
const { notifications } = useNotifications();
const filterHandler = (isFilterIn: boolean) => (): void => { const filterHandler = (isFilterIn: boolean) => (): void => {
if (parentIsArray) { if (parentIsArray) {
@@ -75,18 +80,53 @@ function BodyTitleRenderer({
onClick: onClickHandler, onClick: onClickHandler,
}; };
const handleTextSelection = (e: React.MouseEvent): void => { const handleNodeClick = useCallback(
// Prevent tree node click when user is trying to select text (e: React.MouseEvent): void => {
// Prevent tree node expansion/collapse
e.stopPropagation(); e.stopPropagation();
}; const cleanedKey = removeObjectFromString(nodeKey);
let copyText: string;
// Check if value is an object or array
const isObject = typeof value === 'object' && value !== null;
if (isObject) {
// For objects/arrays, stringify the entire structure
copyText = `"${cleanedKey}": ${JSON.stringify(value, null, 2)}`;
} else if (parentIsArray) {
// For array elements, copy just the value
copyText = `"${cleanedKey}": ${value}`;
} else {
// For primitive values, format as JSON key-value pair
const valueStr = typeof value === 'string' ? `"${value}"` : String(value);
copyText = `"${cleanedKey}": ${valueStr}`;
}
setCopy(copyText);
if (copyText) {
const notificationMessage = isObject
? `${cleanedKey} object copied to clipboard`
: `${cleanedKey} copied to clipboard`;
notifications.success({
message: notificationMessage,
key: notificationMessage,
});
}
},
[nodeKey, parentIsArray, setCopy, value, notifications],
);
return ( return (
<TitleWrapper onMouseDown={handleTextSelection}> <TitleWrapper onClick={handleNodeClick}>
{typeof value !== 'object' && (
<Dropdown menu={menu} trigger={['click']}> <Dropdown menu={menu} trigger={['click']}>
<SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" /> <SettingOutlined style={{ marginRight: 8 }} className="hover-reveal" />
</Dropdown> </Dropdown>
)}
{title.toString()}{' '} {title.toString()}{' '}
{!parentIsArray && ( {!parentIsArray && typeof value !== 'object' && (
<span> <span>
: <span style={{ color: orange[6] }}>{`${value}`}</span> : <span style={{ color: orange[6] }}>{`${value}`}</span>
</span> </span>

View File

@@ -202,9 +202,7 @@ export default function TableViewActions(
if (record.field === 'body') { if (record.field === 'body') {
return ( return (
<div className={cx('value-field', isOpen ? 'open-popover' : '')}> <div className={cx('value-field', isOpen ? 'open-popover' : '')}>
<CopyClipboardHOC entityKey={fieldFilterKey} textToCopy={textToCopy}>
<BodyContent fieldData={fieldData} record={record} bodyHtml={bodyHtml} /> <BodyContent fieldData={fieldData} record={record} bodyHtml={bodyHtml} />
</CopyClipboardHOC>
{!isListViewPanel && !RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey) && ( {!isListViewPanel && !RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey) && (
<span className="action-btn"> <span className="action-btn">
<Tooltip title="Filter for value"> <Tooltip title="Filter for value">

View File

@@ -0,0 +1,109 @@
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import BodyTitleRenderer from '../BodyTitleRenderer';
let mockSetCopy: jest.Mock;
const mockNotification = jest.fn();
jest.mock('hooks/logs/useActiveLog', () => ({
useActiveLog: (): any => ({
onAddToQuery: jest.fn(),
}),
}));
jest.mock('react-use', () => ({
useCopyToClipboard: (): any => {
mockSetCopy = jest.fn();
return [{ value: null }, mockSetCopy];
},
}));
jest.mock('hooks/useNotifications', () => ({
useNotifications: (): any => ({
notifications: {
success: mockNotification,
error: jest.fn(),
info: jest.fn(),
warning: jest.fn(),
open: jest.fn(),
destroy: jest.fn(),
},
}),
}));
describe('BodyTitleRenderer', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should copy primitive value when node is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<BodyTitleRenderer
title="name"
nodeKey="user.name"
value="John"
parentIsArray={false}
/>,
);
await user.click(screen.getByText('name'));
await waitFor(() => {
expect(mockSetCopy).toHaveBeenCalledWith('"user.name": "John"');
expect(mockNotification).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('user.name'),
}),
);
});
});
it('should copy array element value when clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<BodyTitleRenderer
title="0"
nodeKey="items[*].0"
value="arrayElement"
parentIsArray
/>,
);
await user.click(screen.getByText('0'));
await waitFor(() => {
expect(mockSetCopy).toHaveBeenCalledWith('"items[*].0": arrayElement');
});
});
it('should copy entire object when object node is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const testObject = { id: 123, active: true };
render(
<BodyTitleRenderer
title="metadata"
nodeKey="user.metadata"
value={testObject}
parentIsArray={false}
/>,
);
await user.click(screen.getByText('metadata'));
await waitFor(() => {
const callArg = mockSetCopy.mock.calls[0][0];
expect(callArg).toContain('"user.metadata":');
expect(callArg).toContain('"id": 123');
expect(callArg).toContain('"active": true');
expect(mockNotification).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('object copied'),
}),
);
});
});
});

View File

@@ -39,9 +39,17 @@ export const computeDataNode = (
valueIsArray: boolean, valueIsArray: boolean,
value: unknown, value: unknown,
nodeKey: string, nodeKey: string,
parentIsArray: boolean,
): DataNode => ({ ): DataNode => ({
key: uniqueId(), key: uniqueId(),
title: `${key} ${valueIsArray ? '[...]' : ''}`, title: (
<BodyTitleRenderer
title={`${key} ${valueIsArray ? '[...]' : ''}`}
nodeKey={nodeKey}
value={value}
parentIsArray={parentIsArray}
/>
),
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
children: jsonToDataNodes( children: jsonToDataNodes(
value as Record<string, unknown>, value as Record<string, unknown>,
@@ -67,7 +75,7 @@ export function jsonToDataNodes(
if (parentIsArray) { if (parentIsArray) {
if (typeof value === 'object' && value !== null) { if (typeof value === 'object' && value !== null) {
return computeDataNode(key, valueIsArray, value, nodeKey); return computeDataNode(key, valueIsArray, value, nodeKey, parentIsArray);
} }
return { return {
@@ -85,7 +93,7 @@ export function jsonToDataNodes(
} }
if (typeof value === 'object' && value !== null) { if (typeof value === 'object' && value !== null) {
return computeDataNode(key, valueIsArray, value, nodeKey); return computeDataNode(key, valueIsArray, value, nodeKey, parentIsArray);
} }
return { return {
key: uniqueId(), key: uniqueId(),

View File

@@ -209,6 +209,15 @@
} }
} }
} }
.time-series-view-container {
.time-series-view-container-header {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 12px;
}
}
} }
} }

View File

@@ -22,6 +22,7 @@ import {
getListQuery, getListQuery,
getQueryByPanelType, getQueryByPanelType,
} from 'container/LogsExplorerViews/explorerUtils'; } from 'container/LogsExplorerViews/explorerUtils';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView'; import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { useCopyLogLink } from 'hooks/logs/useCopyLogLink'; import { useCopyLogLink } from 'hooks/logs/useCopyLogLink';
import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange'; import { useGetExplorerQueryRange } from 'hooks/queryBuilder/useGetExplorerQueryRange';
@@ -110,6 +111,8 @@ function LogsExplorerViewsContainer({
const [orderBy, setOrderBy] = useState<string>('timestamp:desc'); const [orderBy, setOrderBy] = useState<string>('timestamp:desc');
const [yAxisUnit, setYAxisUnit] = useState<string>('');
const listQuery = useMemo(() => getListQuery(stagedQuery) || null, [ const listQuery = useMemo(() => getListQuery(stagedQuery) || null, [
stagedQuery, stagedQuery,
]); ]);
@@ -350,6 +353,10 @@ function LogsExplorerViewsContainer({
orderBy, orderBy,
]); ]);
const onUnitChangeHandler = useCallback((value: string): void => {
setYAxisUnit(value);
}, []);
const chartData = useMemo(() => { const chartData = useMemo(() => {
if (!stagedQuery) return []; if (!stagedQuery) return [];
@@ -457,15 +464,24 @@ function LogsExplorerViewsContainer({
)} )}
{selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && ( {selectedPanelType === PANEL_TYPES.TIME_SERIES && !showLiveLogs && (
<div className="time-series-view-container">
<div className="time-series-view-container-header">
<BuilderUnitsFilter
onChange={onUnitChangeHandler}
yAxisUnit={yAxisUnit}
/>
</div>
<TimeSeriesView <TimeSeriesView
isLoading={isLoading || isFetching} isLoading={isLoading || isFetching}
data={data} data={data}
isError={isError} isError={isError}
error={error as APIError} error={error as APIError}
yAxisUnit={yAxisUnit}
isFilterApplied={!isEmpty(listQuery?.filters?.items)} isFilterApplied={!isEmpty(listQuery?.filters?.items)}
dataSource={DataSource.LOGS} dataSource={DataSource.LOGS}
setWarning={setWarning} setWarning={setWarning}
/> />
</div>
)} )}
{selectedPanelType === PANEL_TYPES.TABLE && !showLiveLogs && ( {selectedPanelType === PANEL_TYPES.TABLE && !showLiveLogs && (

View File

@@ -68,7 +68,7 @@
display: flex; display: flex;
gap: 6px; gap: 6px;
align-items: center; align-items: center;
max-width: 100%; max-width: 80%;
.dashboard-btn { .dashboard-btn {
display: flex; display: flex;
@@ -130,7 +130,6 @@
.left-section { .left-section {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
width: 45%; width: 45%;
@@ -148,16 +147,17 @@
font-weight: 500; font-weight: 500;
line-height: 24px; /* 150% */ line-height: 24px; /* 150% */
letter-spacing: -0.08px; letter-spacing: -0.08px;
flex-shrink: 0; max-width: 80%;
flex: 1;
min-width: fit-content;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 3; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
} }
.public-dashboard-icon {
margin-right: 4px;
}
} }
.right-section { .right-section {

View File

@@ -17,7 +17,7 @@ function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
setVisible(true); setVisible(true);
}; };
const onClose = (): void => { const handleClose = (): void => {
setVisible(false); setVisible(false);
variableViewModeRef?.current?.(); variableViewModeRef?.current?.();
}; };
@@ -38,7 +38,7 @@ function SettingsDrawer({ drawerTitle }: { drawerTitle: string }): JSX.Element {
title={drawerTitle} title={drawerTitle}
placement="right" placement="right"
width="50%" width="50%"
onClose={onClose} onClose={handleClose}
open={visible} open={visible}
rootClassName="settings-container-root" rootClassName="settings-container-root"
> >

View File

@@ -17,6 +17,7 @@ import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes'; import ROUTES from 'constants/routes';
import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteButton'; import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteButton';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2'; import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta';
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
import useComponentPermission from 'hooks/useComponentPermission'; import useComponentPermission from 'hooks/useComponentPermission';
import { useNotifications } from 'hooks/useNotifications'; import { useNotifications } from 'hooks/useNotifications';
@@ -29,6 +30,7 @@ import {
FileJson, FileJson,
FolderKanban, FolderKanban,
Fullscreen, Fullscreen,
Globe,
LayoutGrid, LayoutGrid,
LockKeyhole, LockKeyhole,
PenLine, PenLine,
@@ -128,6 +130,8 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
false, false,
); );
const [isPublicDashboard, setIsPublicDashboard] = useState<boolean>(false);
let isAuthor = false; let isAuthor = false;
if (selectedDashboard && user && user.email) { if (selectedDashboard && user && user.email) {
@@ -297,6 +301,38 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
safeNavigate(generatedUrl); safeNavigate(generatedUrl);
} }
const {
data: publicDashboardResponse,
// refetch: refetchPublicDashboardData,
isLoading: isLoadingPublicDashboardData,
isFetching: isFetchingPublicDashboardData,
error: errorPublicDashboardData,
isError: isErrorPublicDashboardData,
} = useGetPublicDashboardMeta(selectedDashboard?.id || '');
useEffect(() => {
if (!isLoadingPublicDashboardData && !isFetchingPublicDashboardData) {
if (isErrorPublicDashboardData) {
const errorDetails = errorPublicDashboardData?.getErrorDetails();
if (errorDetails?.error?.code === 'public_dashboard_not_found') {
setIsPublicDashboard(false);
}
} else {
const publicDashboardData = publicDashboardResponse?.data;
if (publicDashboardData?.publicPath) {
setIsPublicDashboard(true);
}
}
}
}, [
isLoadingPublicDashboardData,
isFetchingPublicDashboardData,
isErrorPublicDashboardData,
errorPublicDashboardData,
publicDashboardResponse?.data,
]);
return ( return (
<Card className="dashboard-description-container"> <Card className="dashboard-description-container">
<div className="dashboard-header"> <div className="dashboard-header">
@@ -333,11 +369,21 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
className="dashboard-title" className="dashboard-title"
data-testid="dashboard-title" data-testid="dashboard-title"
> >
{' '}
{title} {title}
</Typography.Text> </Typography.Text>
</Tooltip> </Tooltip>
{isDashboardLocked && <LockKeyhole size={14} />}
{isPublicDashboard && (
<Tooltip title="This dashboard is publicly accessible">
<Globe size={14} className="public-dashboard-icon" />
</Tooltip>
)}
{isDashboardLocked && (
<Tooltip title="This dashboard is locked">
<LockKeyhole size={14} className="lock-dashboard-icon" />
</Tooltip>
)}
</div> </div>
<div className="right-section"> <div className="right-section">
<DateTimeSelectionV2 showAutoRefresh hideShareModal /> <DateTimeSelectionV2 showAutoRefresh hideShareModal />

View File

@@ -1,6 +1,5 @@
.settings-tabs { .settings-tabs {
.ant-tabs-nav-list { .ant-tabs-nav-list {
width: 228px;
height: 32px; height: 32px;
flex-shrink: 0; flex-shrink: 0;
border-radius: 2px; border-radius: 2px;
@@ -13,6 +12,10 @@
margin: 0px; margin: 0px;
} }
.ant-tabs-tab:not(:last-child) {
border-right: 1px solid var(--bg-slate-400) !important;
}
.overview-btn { .overview-btn {
width: 114px; width: 114px;
display: flex; display: flex;
@@ -27,6 +30,13 @@
justify-content: center; justify-content: center;
} }
.public-dashboard-btn {
width: 150px;
display: flex;
align-items: center;
justify-content: center;
}
.ant-tabs-ink-bar { .ant-tabs-ink-bar {
display: none; display: none;
} }
@@ -41,6 +51,11 @@
border-radius: 2px 0px 0px 2px; border-radius: 2px 0px 0px 2px;
background: var(--bg-slate-400); background: var(--bg-slate-400);
} }
.public-dashboard-btn {
border-radius: 2px 0px 0px 2px;
background: var(--bg-slate-400);
}
} }
} }
@@ -63,6 +78,10 @@
.variables-btn { .variables-btn {
background: var(--bg-vanilla-300); background: var(--bg-vanilla-300);
} }
.public-dashboard-btn {
background: var(--bg-vanilla-300);
}
} }
} }
} }

View File

@@ -0,0 +1,132 @@
.public-dashboard-setting-container {
border-radius: 3px;
border: 1px solid var(--bg-slate-500);
padding: 16px !important;
.public-dashboard-setting-content {
display: flex;
flex-direction: column;
gap: 8px;
.public-dashboard-setting-content-title {
margin-bottom: 16px;
}
.timerange-enabled-checkbox {
margin-bottom: 8px;
}
.default-time-range-select {
margin-bottom: 8px;
.default-time-range-select-label {
margin-bottom: 4px;
.default-time-range-select-label-text {
font-size: 12px;
font-weight: 500;
}
}
.ant-select-selector {
display: flex;
height: 32px;
padding: 6px;
justify-content: center;
align-items: center;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
.ant-select-selection-item {
display: flex;
align-items: center;
.list-item-image {
height: 16px;
width: 16px;
}
}
}
}
.public-dashboard-url {
.url-label-container {
margin-bottom: 4px;
.url-label {
font-size: 12px;
font-weight: 500;
}
}
.url-container {
display: flex;
align-items: center;
gap: 8px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
border-radius: 4px;
padding: 0px 4px;
.url-text {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
.default-time-range-select-dropdown {
width: 200px;
}
.public-dashboard-setting-callout {
margin-top: 12px;
background: color-mix(in srgb, var(--bg-robin-500) 10%, transparent);
padding: 12px 8px;
border-radius: 3px;
.public-dashboard-setting-callout-text {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 400;
color: var(--text-robin-300);
}
}
}
.public-dashboard-setting-actions {
margin-top: 32px;
display: flex;
gap: 8px;
justify-content: flex-end;
}
}
.lightMode {
.public-dashboard-setting-container {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
.public-dashboard-setting-content {
.default-time-range-select {
.ant-select-selector {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
.public-dashboard-url {
.url-container {
border: 1px solid var(--bg-vanilla-300);
background: var(--bg-vanilla-100);
}
}
}
}
}

View File

@@ -0,0 +1,382 @@
import { toast } from '@signozhq/sonner';
import { fireEvent, within } from '@testing-library/react';
import { StatusCodes } from 'http-status-codes';
import {
publishedPublicDashboardMeta,
unpublishedPublicDashboardMeta,
} from 'mocks-server/__mockdata__/publicDashboard';
import { rest, server } from 'mocks-server/server';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCopyToClipboard } from 'react-use';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import PublicDashboardSetting from '../index';
// Mock dependencies
jest.mock('providers/Dashboard/Dashboard');
jest.mock('react-use', () => ({
...jest.requireActual('react-use'),
useCopyToClipboard: jest.fn(),
}));
jest.mock('@signozhq/sonner', () => ({
toast: {
success: jest.fn(),
error: jest.fn(),
},
}));
const mockUseDashboard = jest.mocked(useDashboard);
const mockUseCopyToClipboard = jest.mocked(useCopyToClipboard);
const mockToast = jest.mocked(toast);
// Test constants
const MOCK_DASHBOARD_ID = 'test-dashboard-id';
const MOCK_PUBLIC_PATH = '/public/dashboard/test-dashboard-id';
const DEFAULT_TIME_RANGE = '30m';
const DASHBOARD_VARIABLES_WARNING =
"Dashboard variables won't work in public dashboards";
// Use wildcard pattern to match both relative and absolute URLs in MSW
const publicDashboardURL = `*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`;
const mockSelectedDashboard = {
id: MOCK_DASHBOARD_ID,
data: {
title: 'Test Dashboard',
widgets: [],
layout: [],
panelMap: {},
variables: {},
},
};
beforeAll(() => {
server.listen();
});
afterAll(() => {
server.close();
});
const mockSetCopyPublicDashboardURL = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
// Mock window.open
window.open = jest.fn();
// Mock useDashboard
mockUseDashboard.mockReturnValue(({
selectedDashboard: mockSelectedDashboard,
} as unknown) as ReturnType<typeof useDashboard>);
// Mock useCopyToClipboard
mockUseCopyToClipboard.mockReturnValue(([
undefined,
mockSetCopyPublicDashboardURL,
] as unknown) as ReturnType<typeof useCopyToClipboard>);
});
afterEach(() => {
server.resetHandlers();
jest.clearAllMocks();
});
describe('PublicDashboardSetting', () => {
describe('Unpublished Dashboard', () => {
it('Unpublished dashboard should be handled correctly', async () => {
server.use(
rest.get(
`*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`,
(_req, res, ctx) =>
res(
ctx.status(StatusCodes.NOT_FOUND),
ctx.json(unpublishedPublicDashboardMeta),
),
),
);
render(<PublicDashboardSetting />);
await waitFor(() => {
expect(
screen.getByText(
/This dashboard is private. Publish it to make it accessible to anyone with the link./i,
),
).toBeInTheDocument();
});
await waitFor(() => {
expect(
screen.getByRole('checkbox', { name: /enable time range/i }),
).toBeInTheDocument();
});
await waitFor(() => {
expect(screen.getByText(/default time range/i)).toBeInTheDocument();
});
expect(screen.getByText(/Last 30 minutes/i)).toBeInTheDocument();
await waitFor(() => {
expect(
screen.getByText(new RegExp(DASHBOARD_VARIABLES_WARNING, 'i')),
).toBeInTheDocument();
});
expect(
screen.getByRole('button', { name: /publish dashboard/i }),
).toBeInTheDocument();
});
});
describe('Published Dashboard', () => {
it('Published dashboard should be handled correctly', async () => {
server.use(
rest.get(
`*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`,
(_req, res, ctx) =>
res(ctx.status(StatusCodes.OK), ctx.json(publishedPublicDashboardMeta)),
),
);
render(<PublicDashboardSetting />);
await waitFor(() => {
expect(
screen.getByText(
/This dashboard is publicly accessible. Anyone with the link can view it./i,
),
).toBeInTheDocument();
});
await waitFor(() => {
expect(
screen.getByRole('checkbox', { name: /enable time range/i }),
).toBeChecked();
});
await waitFor(() => {
expect(screen.getByText(/default time range/i)).toBeInTheDocument();
});
expect(screen.getByText(/Last 30 minutes/i)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/Public Dashboard URL/i)).toBeInTheDocument();
});
await waitFor(() => {
expect(
screen.getByRole('button', { name: /update published dashboard/i }),
).toBeInTheDocument();
});
await waitFor(() => {
expect(
screen.getByRole('button', { name: /unpublish dashboard/i }),
).toBeInTheDocument();
});
});
});
describe('Time Range Settings', () => {
beforeEach(() => {
server.use(
rest.get(
`*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`,
(_req, res, ctx) =>
res(ctx.status(StatusCodes.OK), ctx.json(publishedPublicDashboardMeta)),
),
);
});
it('should toggle time range enabled when checkbox is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<PublicDashboardSetting />);
// Wait for checkbox to be rendered and verify initial state
const checkbox = await screen.findByRole('checkbox', {
name: /enable time range/i,
});
expect(checkbox).toBeChecked();
await user.click(checkbox);
await waitFor(() => {
expect(checkbox).not.toBeChecked();
});
});
it('should update default time range when select value changes', async () => {
render(<PublicDashboardSetting />);
const selectContainer = await screen.findByTestId(
'default-time-range-select-dropdown',
);
const combobox = within(selectContainer).getByRole('combobox');
fireEvent.mouseDown(combobox);
await screen.findByRole('listbox');
const option = await screen.findByText(/Last 1 hour/i, {
selector: '.ant-select-item-option-content',
});
fireEvent.click(option);
await waitFor(() => {
expect(
within(selectContainer).getByText(/Last 1 hour/i),
).toBeInTheDocument();
});
});
});
describe('Create Public Dashboard', () => {
it('should call create API when publish button is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
let createApiCalled = false;
server.use(
rest.get(publicDashboardURL, (_req, res, ctx) =>
res(
ctx.status(StatusCodes.OK),
ctx.json({
data: {
timeRangeEnabled: true,
defaultTimeRange: DEFAULT_TIME_RANGE,
publicPath: '',
},
}),
),
),
rest.post(publicDashboardURL, async (req, res, ctx) => {
const body = await req.json();
createApiCalled = true;
expect(body).toEqual({
timeRangeEnabled: true,
defaultTimeRange: DEFAULT_TIME_RANGE,
});
return res(
ctx.status(StatusCodes.CREATED),
ctx.json({
data: {
timeRangeEnabled: true,
defaultTimeRange: DEFAULT_TIME_RANGE,
publicPath: MOCK_PUBLIC_PATH,
},
}),
);
}),
);
render(<PublicDashboardSetting />);
// Find and click publish button
const publishButton = await screen.findByRole('button', {
name: /publish dashboard/i,
});
await user.click(publishButton);
await waitFor(() => {
expect(createApiCalled).toBe(true);
expect(mockToast.success).toHaveBeenCalledWith(
'Public dashboard created successfully',
);
});
});
});
describe('Update Public Dashboard', () => {
it('should call update API when update button is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
let updateApiCalled = false;
let capturedRequestBody: {
timeRangeEnabled: boolean;
defaultTimeRange: string;
} | null = null;
server.use(
rest.get(
`*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`,
(_req, res, ctx) =>
res(ctx.status(StatusCodes.OK), ctx.json(publishedPublicDashboardMeta)),
),
rest.put(publicDashboardURL, async (req, res, ctx) => {
const body = await req.json();
updateApiCalled = true;
capturedRequestBody = body;
return res(ctx.status(StatusCodes.NO_CONTENT), ctx.json({}));
}),
);
render(<PublicDashboardSetting />);
// Wait for API response and component update
const updateButton = await screen.findByRole(
'button',
{ name: /update published dashboard/i },
{ timeout: 5000 },
);
await user.click(updateButton);
await waitFor(() => {
expect(updateApiCalled).toBe(true);
expect(capturedRequestBody).toEqual({
timeRangeEnabled: true,
defaultTimeRange: DEFAULT_TIME_RANGE,
});
expect(mockToast.success).toHaveBeenCalledWith(
'Public dashboard updated successfully',
);
});
});
});
describe('Revoke Public Dashboard Access', () => {
it('should call revoke API when unpublish button is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
let revokeApiCalled = false;
let capturedDashboardId: string | null = null;
server.use(
rest.get(
`*/api/v1/dashboards/${MOCK_DASHBOARD_ID}/public`,
(_req, res, ctx) =>
res(ctx.status(StatusCodes.OK), ctx.json(publishedPublicDashboardMeta)),
),
rest.delete(publicDashboardURL, (req, res, ctx) => {
revokeApiCalled = true;
// Extract dashboard ID from URL: /api/v1/dashboards/{id}/public
const urlMatch = req.url.pathname.match(
/\/api\/v1\/dashboards\/([^/]+)\/public/,
);
capturedDashboardId = urlMatch ? urlMatch[1] : null;
return res(ctx.status(StatusCodes.NO_CONTENT), ctx.json({}));
}),
);
render(<PublicDashboardSetting />);
// Wait for API response and component update
const unpublishButton = await screen.findByRole(
'button',
{ name: /unpublish dashboard/i },
{ timeout: 5000 },
);
await user.click(unpublishButton);
await waitFor(() => {
expect(revokeApiCalled).toBe(true);
expect(capturedDashboardId).toBe(MOCK_DASHBOARD_ID);
expect(mockToast.success).toHaveBeenCalledWith(
'Dashboard unpublished successfully',
);
});
});
});
});

View File

@@ -0,0 +1,338 @@
/* eslint-disable import/no-extraneous-dependencies */
import './PublicDashboard.styles.scss';
import { Checkbox } from '@signozhq/checkbox';
import { toast } from '@signozhq/sonner';
import { Button, Select, Typography } from 'antd';
import createPublicDashboardAPI from 'api/dashboard/public/createPublicDashboard';
import revokePublicDashboardAccessAPI from 'api/dashboard/public/revokePublicDashboardAccess';
import updatePublicDashboardAPI from 'api/dashboard/public/updatePublicDashboard';
import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta';
import { Copy, ExternalLink, Globe, Info, Loader2, Trash } from 'lucide-react';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useMutation } from 'react-query';
import { useCopyToClipboard } from 'react-use';
import { PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
export const TIME_RANGE_PRESETS_OPTIONS = [
{
label: 'Last 5 minutes',
value: '5m',
},
{
label: 'Last 15 minutes',
value: '15m',
},
{
label: 'Last 30 minutes',
value: '30m',
},
{
label: 'Last 1 hour',
value: '1h',
},
{
label: 'Last 6 hours',
value: '6h',
},
{
label: 'Last 1 day',
value: '24h',
},
];
function PublicDashboardSetting(): JSX.Element {
const [publicDashboardData, setPublicDashboardData] = useState<
PublicDashboardMetaProps | undefined
>(undefined);
const [timeRangeEnabled, setTimeRangeEnabled] = useState(true);
const [defaultTimeRange, setDefaultTimeRange] = useState('30m');
const [, setCopyPublicDashboardURL] = useCopyToClipboard();
const { selectedDashboard } = useDashboard();
const handleDefaultTimeRange = useCallback((value: string): void => {
setDefaultTimeRange(value);
}, []);
const handleTimeRangeEnabled = useCallback((): void => {
setTimeRangeEnabled((prev) => !prev);
}, []);
const {
data: publicDashboardResponse,
isLoading: isLoadingPublicDashboard,
isFetching: isFetchingPublicDashboard,
refetch: refetchPublicDashboard,
error: errorPublicDashboard,
} = useGetPublicDashboardMeta(selectedDashboard?.id || '');
const isPublicDashboardEnabled = !!publicDashboardData?.publicPath;
useEffect(() => {
if (publicDashboardResponse?.data) {
setPublicDashboardData(publicDashboardResponse?.data);
}
if (errorPublicDashboard) {
console.error('Error getting public dashboard', errorPublicDashboard);
setPublicDashboardData(undefined);
setTimeRangeEnabled(true);
setDefaultTimeRange('30m');
}
}, [publicDashboardResponse, errorPublicDashboard]);
useEffect(() => {
if (publicDashboardResponse?.data) {
setTimeRangeEnabled(
publicDashboardResponse?.data?.timeRangeEnabled || false,
);
setDefaultTimeRange(
publicDashboardResponse?.data?.defaultTimeRange || '30m',
);
}
}, [publicDashboardResponse]);
const {
mutate: createPublicDashboard,
isLoading: isLoadingCreatePublicDashboard,
data: createPublicDashboardResponse,
} = useMutation(createPublicDashboardAPI, {
onSuccess: () => {
toast.success('Public dashboard created successfully');
},
onError: () => {
toast.error('Failed to create public dashboard');
},
});
const {
mutate: updatePublicDashboard,
isLoading: isLoadingUpdatePublicDashboard,
data: updatePublicDashboardResponse,
} = useMutation(updatePublicDashboardAPI, {
onSuccess: () => {
toast.success('Public dashboard updated successfully');
},
onError: () => {
toast.error('Failed to update public dashboard');
},
});
const {
mutate: revokePublicDashboardAccess,
isLoading: isLoadingRevokePublicDashboardAccess,
data: revokePublicDashboardAccessResponse,
} = useMutation(revokePublicDashboardAccessAPI, {
onSuccess: () => {
toast.success('Dashboard unpublished successfully');
},
onError: () => {
toast.error('Failed to unpublish dashboard');
},
});
const handleCreatePublicDashboard = (): void => {
if (!selectedDashboard) return;
createPublicDashboard({
dashboardId: selectedDashboard.id,
timeRangeEnabled,
defaultTimeRange,
});
};
const handleUpdatePublicDashboard = (): void => {
if (!selectedDashboard) return;
updatePublicDashboard({
dashboardId: selectedDashboard.id,
timeRangeEnabled,
defaultTimeRange,
});
};
const handleRevokePublicDashboardAccess = (): void => {
if (!selectedDashboard) return;
revokePublicDashboardAccess({
id: selectedDashboard.id,
});
};
useEffect(() => {
if (
(createPublicDashboardResponse &&
createPublicDashboardResponse.httpStatusCode === 201) ||
(updatePublicDashboardResponse &&
updatePublicDashboardResponse.httpStatusCode === 204) ||
(revokePublicDashboardAccessResponse &&
revokePublicDashboardAccessResponse.httpStatusCode === 204)
) {
refetchPublicDashboard();
}
}, [
createPublicDashboardResponse,
updatePublicDashboardResponse,
revokePublicDashboardAccessResponse,
refetchPublicDashboard,
]);
const handleCopyPublicDashboardURL = (): void => {
if (!publicDashboardResponse?.data?.publicPath) return;
try {
setCopyPublicDashboardURL(
`${window.location.origin}${publicDashboardResponse?.data?.publicPath}`,
);
toast.success('Copied Public Dashboard URL successfully');
} catch (error) {
console.error('Error copying public dashboard URL', error);
}
};
const publicDashboardURL = useMemo(
() => `${window.location.origin}${publicDashboardResponse?.data?.publicPath}`,
[publicDashboardResponse],
);
const isLoading =
isLoadingCreatePublicDashboard ||
isLoadingUpdatePublicDashboard ||
isLoadingRevokePublicDashboardAccess ||
isLoadingPublicDashboard;
return (
<div className="public-dashboard-setting-container">
<div className="public-dashboard-setting-content">
<Typography.Title
level={5}
className="public-dashboard-setting-content-title"
>
{isPublicDashboardEnabled
? 'This dashboard is publicly accessible. Anyone with the link can view it.'
: 'This dashboard is private. Publish it to make it accessible to anyone with the link.'}
</Typography.Title>
<div className="timerange-enabled-checkbox">
<Checkbox
id="enable-time-range"
checked={timeRangeEnabled}
onCheckedChange={handleTimeRangeEnabled}
labelName="Enable time range"
/>
</div>
<div className="default-time-range-select">
<div className="default-time-range-select-label">
<Typography.Text className="default-time-range-select-label-text">
Default time range
</Typography.Text>
</div>
<Select
placeholder="Select default time range"
options={TIME_RANGE_PRESETS_OPTIONS}
value={defaultTimeRange}
onChange={handleDefaultTimeRange}
data-testid="default-time-range-select-dropdown"
className="default-time-range-select-dropdown"
/>
</div>
{isPublicDashboardEnabled && (
<div className="public-dashboard-url">
<div className="url-label-container">
<Typography.Text className="url-label">
Public Dashboard URL
</Typography.Text>
</div>
<div className="url-container">
<Typography.Text className="url-text">
{publicDashboardURL}
</Typography.Text>
<Button
type="link"
className="url-copy-btn periscope-btn ghost"
icon={<Copy size={12} />}
onClick={handleCopyPublicDashboardURL}
/>
<Button
type="link"
className="periscope-btn ghost"
icon={<ExternalLink size={12} />}
onClick={(): void => {
if (publicDashboardURL) {
window.open(publicDashboardURL, '_blank');
}
}}
/>
</div>
</div>
)}
<div className="public-dashboard-setting-callout">
<Typography.Text className="public-dashboard-setting-callout-text">
<Info size={12} className="public-dashboard-setting-callout-icon" />{' '}
Dashboard variables won&apos;t work in public dashboards
</Typography.Text>
</div>
<div className="public-dashboard-setting-actions">
{!isPublicDashboardEnabled ? (
<Button
type="primary"
className="create-public-dashboard-btn periscope-btn primary"
disabled={isLoading}
onClick={handleCreatePublicDashboard}
loading={
isLoadingCreatePublicDashboard ||
isFetchingPublicDashboard ||
isLoadingPublicDashboard
}
icon={
isLoadingCreatePublicDashboard ||
isFetchingPublicDashboard ||
isLoadingPublicDashboard ? (
<Loader2 className="animate-spin" size={14} />
) : (
<Globe size={14} />
)
}
>
Publish dashboard
</Button>
) : (
<>
<Button
type="default"
className="periscope-btn secondary"
disabled={isLoading}
onClick={handleRevokePublicDashboardAccess}
loading={isLoadingRevokePublicDashboardAccess}
icon={<Trash size={14} />}
>
Unpublish dashboard
</Button>
<Button
type="primary"
className="create-public-dashboard-btn periscope-btn primary"
disabled={isLoading}
onClick={handleUpdatePublicDashboard}
loading={isLoadingUpdatePublicDashboard}
icon={<Globe size={14} />}
>
Update published dashboard
</Button>
</>
)}
</div>
</div>
</div>
);
}
export default PublicDashboardSetting;

View File

@@ -1,9 +1,10 @@
import './DashboardSettingsContent.styles.scss'; import './DashboardSettingsContent.styles.scss';
import { Button, Tabs } from 'antd'; import { Button, Tabs } from 'antd';
import { Braces, Table } from 'lucide-react'; import { Braces, Globe, Table } from 'lucide-react';
import GeneralDashboardSettings from './General'; import GeneralDashboardSettings from './General';
import PublicDashboardSetting from './PublicDashboard';
import VariablesSetting from './Variables'; import VariablesSetting from './Variables';
function DashboardSettingsContent({ function DashboardSettingsContent({
@@ -30,6 +31,19 @@ function DashboardSettingsContent({
key: 'variables', key: 'variables',
children: <VariablesSetting variableViewModeRef={variableViewModeRef} />, children: <VariablesSetting variableViewModeRef={variableViewModeRef} />,
}, },
{
label: (
<Button
type="text"
icon={<Globe size={14} />}
className="public-dashboard-btn"
>
Publish
</Button>
),
key: 'public-dashboard',
children: <PublicDashboardSetting />,
},
]; ];
return <Tabs items={items} animated className="settings-tabs" />; return <Tabs items={items} animated className="settings-tabs" />;

View File

@@ -1,12 +1,17 @@
import { Checkbox, Empty } from 'antd'; import { Checkbox, Empty } from 'antd';
import { AxiosResponse } from 'axios';
import Spinner from 'components/Spinner'; import Spinner from 'components/Spinner';
import { EXCLUDED_COLUMNS } from 'container/OptionsMenu/constants';
import { QueryKeySuggestionsResponseProps } from 'types/api/querySuggestions/types';
import { DataSource } from 'types/common/queryBuilder';
type ExplorerAttributeColumnsProps = { type ExplorerAttributeColumnsProps = {
isLoading: boolean; isLoading: boolean;
data: any; data: AxiosResponse<QueryKeySuggestionsResponseProps> | undefined;
searchText: string; searchText: string;
isAttributeKeySelected: (key: string) => boolean; isAttributeKeySelected: (key: string) => boolean;
handleCheckboxChange: (key: string) => void; handleCheckboxChange: (key: string) => void;
dataSource: DataSource;
}; };
function ExplorerAttributeColumns({ function ExplorerAttributeColumns({
@@ -15,6 +20,7 @@ function ExplorerAttributeColumns({
searchText, searchText,
isAttributeKeySelected, isAttributeKeySelected,
handleCheckboxChange, handleCheckboxChange,
dataSource,
}: ExplorerAttributeColumnsProps): JSX.Element { }: ExplorerAttributeColumnsProps): JSX.Element {
if (isLoading) { if (isLoading) {
return ( return (
@@ -27,8 +33,10 @@ function ExplorerAttributeColumns({
const filteredAttributeKeys = const filteredAttributeKeys =
Object.values(data?.data?.data?.keys || {}) Object.values(data?.data?.data?.keys || {})
?.flat() ?.flat()
?.filter((attributeKey: any) => ?.filter(
attributeKey.name.toLowerCase().includes(searchText.toLowerCase()), (attributeKey) =>
attributeKey.name.toLowerCase().includes(searchText.toLowerCase()) &&
!EXCLUDED_COLUMNS[dataSource].includes(attributeKey.name),
) || []; ) || [];
if (filteredAttributeKeys.length === 0) { if (filteredAttributeKeys.length === 0) {
return ( return (

View File

@@ -183,6 +183,7 @@ function ExplorerColumnsRenderer({
searchText={searchText} searchText={searchText}
isAttributeKeySelected={isAttributeKeySelected} isAttributeKeySelected={isAttributeKeySelected}
handleCheckboxChange={handleCheckboxChange} handleCheckboxChange={handleCheckboxChange}
dataSource={initialDataSource}
/> />
), ),
}, },

View File

@@ -450,4 +450,58 @@ describe('ExplorerColumnsRenderer', () => {
} }
}); });
}); });
it('does not show isRoot or isEntryPoint in add column dropdown (traces, dashboard table panel)', async () => {
(useQueryBuilder as jest.Mock).mockReturnValue({
currentQuery: {
builder: {
queryData: [
{
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
},
],
},
},
});
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
data: {
data: {
keys: {
attributeKeys: [
{ name: 'isRoot', dataType: 'bool', type: '' },
{ name: 'isEntryPoint', dataType: 'bool', type: '' },
{ name: 'duration', dataType: 'number', type: '' },
{ name: 'serviceName', dataType: 'string', type: '' },
],
},
},
},
},
isLoading: false,
isError: false,
});
render(
<Wrapper>
<ExplorerColumnsRenderer
selectedLogFields={[]}
setSelectedLogFields={mockSetSelectedLogFields}
selectedTracesFields={[]}
setSelectedTracesFields={mockSetSelectedTracesFields}
/>
</Wrapper>,
);
await userEvent.click(screen.getByTestId('add-columns-button'));
// Visible columns should appear
expect(screen.getByText('duration')).toBeInTheDocument();
expect(screen.getByText('serviceName')).toBeInTheDocument();
// Hidden columns should NOT appear
expect(screen.queryByText('isRoot')).not.toBeInTheDocument();
expect(screen.queryByText('isEntryPoint')).not.toBeInTheDocument();
});
}); });

View File

@@ -0,0 +1,235 @@
import { renderHook } from '@testing-library/react';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQueryData from 'hooks/useUrlQueryData';
import { usePreferenceContext } from 'providers/preferences/context/PreferenceContextProvider';
import { useQueries } from 'react-query';
import { DataSource } from 'types/common/queryBuilder';
import useOptionsMenu from '../useOptionsMenu';
// Mock all dependencies
jest.mock('hooks/useNotifications');
jest.mock('providers/preferences/context/PreferenceContextProvider');
jest.mock('hooks/useUrlQueryData');
jest.mock('hooks/querySuggestions/useGetQueryKeySuggestions');
jest.mock('react-query', () => ({
...jest.requireActual('react-query'),
useQueries: jest.fn(),
}));
describe('useOptionsMenu', () => {
const mockNotifications = { error: jest.fn(), success: jest.fn() };
const mockUpdateColumns = jest.fn();
const mockUpdateFormatting = jest.fn();
const mockRedirectWithQuery = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
(useNotifications as jest.Mock).mockReturnValue({
notifications: mockNotifications,
});
(usePreferenceContext as jest.Mock).mockReturnValue({
traces: {
preferences: {
columns: [],
formatting: {
format: 'raw',
maxLines: 2,
fontSize: 'small',
},
},
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
logs: {
preferences: {
columns: [],
formatting: {
format: 'raw',
maxLines: 2,
fontSize: 'small',
},
},
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
});
(useUrlQueryData as jest.Mock).mockReturnValue({
query: null,
redirectWithQuery: mockRedirectWithQuery,
});
(useQueries as jest.Mock).mockReturnValue([]);
});
it('does not show isRoot or isEntryPoint in column options when dataSource is TRACES', () => {
// Mock the query key suggestions to return data including isRoot and isEntryPoint
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
data: {
data: {
keys: {
attributeKeys: [
{
name: 'isRoot',
signal: 'traces',
fieldDataType: 'bool',
fieldContext: '',
},
{
name: 'isEntryPoint',
signal: 'traces',
fieldDataType: 'bool',
fieldContext: '',
},
{
name: 'duration',
signal: 'traces',
fieldDataType: 'float64',
fieldContext: '',
},
{
name: 'serviceName',
signal: 'traces',
fieldDataType: 'string',
fieldContext: '',
},
],
},
},
},
},
isFetching: false,
});
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
}),
);
// Get the column options from the config
const columnOptions = result.current.config.addColumn?.options ?? [];
const optionNames = columnOptions.map((option) => option.label);
// isRoot and isEntryPoint should NOT be in the options
expect(optionNames).not.toContain('isRoot');
expect(optionNames).not.toContain('body');
expect(optionNames).not.toContain('isEntryPoint');
// Other attributes should be present
expect(optionNames).toContain('duration');
expect(optionNames).toContain('serviceName');
});
it('does not show body in column options when dataSource is METRICS', () => {
// Mock the query key suggestions to return data including body
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
data: {
data: {
keys: {
attributeKeys: [
{
name: 'body',
signal: 'logs',
fieldDataType: 'string',
fieldContext: '',
},
{
name: 'status',
signal: 'metrics',
fieldDataType: 'int64',
fieldContext: '',
},
{
name: 'value',
signal: 'metrics',
fieldDataType: 'float64',
fieldContext: '',
},
],
},
},
},
},
isFetching: false,
});
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.METRICS,
aggregateOperator: 'count',
}),
);
// Get the column options from the config
const columnOptions = result.current.config.addColumn?.options ?? [];
const optionNames = columnOptions.map((option) => option.label);
// body should NOT be in the options
expect(optionNames).not.toContain('body');
// Other attributes should be present
expect(optionNames).toContain('status');
expect(optionNames).toContain('value');
});
it('does not show body in column options when dataSource is LOGS', () => {
// Mock the query key suggestions to return data including body
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: {
data: {
data: {
keys: {
attributeKeys: [
{
name: 'body',
signal: 'logs',
fieldDataType: 'string',
fieldContext: '',
},
{
name: 'level',
signal: 'logs',
fieldDataType: 'string',
fieldContext: '',
},
{
name: 'timestamp',
signal: 'logs',
fieldDataType: 'int64',
fieldContext: '',
},
],
},
},
},
},
isFetching: false,
});
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.LOGS,
aggregateOperator: 'count',
}),
);
// Get the column options from the config
const columnOptions = result.current.config.addColumn?.options ?? [];
const optionNames = columnOptions.map((option) => option.label);
// body should be in the options
expect(optionNames).toContain('body');
// Other attributes should be present
expect(optionNames).toContain('level');
expect(optionNames).toContain('timestamp');
});
});

View File

@@ -1,4 +1,5 @@
import { TelemetryFieldKey } from 'api/v5/v5'; import { TelemetryFieldKey } from 'api/v5/v5';
import { DataSource } from 'types/common/queryBuilder';
import { FontSize, OptionsQuery } from './types'; import { FontSize, OptionsQuery } from './types';
@@ -11,6 +12,12 @@ export const defaultOptionsQuery: OptionsQuery = {
fontSize: FontSize.SMALL, fontSize: FontSize.SMALL,
}; };
export const EXCLUDED_COLUMNS: Record<DataSource, string[]> = {
[DataSource.TRACES]: ['body', 'isRoot', 'isEntryPoint'],
[DataSource.METRICS]: ['body'],
[DataSource.LOGS]: [],
};
export const defaultLogsSelectedColumns: TelemetryFieldKey[] = [ export const defaultLogsSelectedColumns: TelemetryFieldKey[] = [
{ {
name: 'timestamp', name: 'timestamp',

View File

@@ -27,6 +27,7 @@ import {
defaultLogsSelectedColumns, defaultLogsSelectedColumns,
defaultOptionsQuery, defaultOptionsQuery,
defaultTraceSelectedColumns, defaultTraceSelectedColumns,
EXCLUDED_COLUMNS,
URL_OPTIONS, URL_OPTIONS,
} from './constants'; } from './constants';
import { import {
@@ -267,8 +268,9 @@ const useOptionsMenu = ({
const optionsFromAttributeKeys = useMemo(() => { const optionsFromAttributeKeys = useMemo(() => {
const filteredAttributeKeys = searchedAttributeKeys.filter((item) => { const filteredAttributeKeys = searchedAttributeKeys.filter((item) => {
if (dataSource !== DataSource.LOGS) { const exclusions = EXCLUDED_COLUMNS[dataSource];
return item.name !== 'body'; if (exclusions) {
return !exclusions.includes(item.name);
} }
return true; return true;
}); });

View File

@@ -0,0 +1,146 @@
import { ENTITY_VERSION_V5 } from 'constants/app';
import { PANEL_TYPES } from 'constants/queryBuilder';
import EmptyWidget from 'container/GridCardLayout/EmptyWidget';
import WidgetGraphComponent from 'container/GridCardLayout/GridCard/WidgetGraphComponent';
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { memo, useCallback, useMemo, useRef } from 'react';
import { Widgets } from 'types/api/dashboard/getAll';
import { DataSource } from 'types/common/queryBuilder';
import { getGraphType } from 'utils/getGraphType';
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
function Panel({
widget,
index,
dashboardId,
startTime,
endTime,
}: {
widget: Widgets;
index: number;
dashboardId: string;
startTime: number;
endTime: number;
}): JSX.Element {
const graphRef = useRef<HTMLDivElement>(null);
const updatedQuery = widget?.query;
const requestData: GetQueryResultsProps = useMemo(() => {
if (widget.panelTypes !== PANEL_TYPES.LIST) {
return {
selectedTime: widget?.timePreferance,
graphType: getGraphType(widget.panelTypes),
query: updatedQuery,
variables: {}, // we are not supporting variables in public dashboards
fillGaps: widget.fillSpans,
formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE,
start: startTime,
end: endTime,
originalGraphType: widget.panelTypes,
};
}
updatedQuery.builder.queryData[0].pageSize = 10;
const initialDataSource = updatedQuery.builder.queryData[0].dataSource;
return {
query: updatedQuery,
graphType: PANEL_TYPES.LIST,
selectedTime: widget.timePreferance || 'GLOBAL_TIME',
tableParams: {
pagination: {
offset: 0,
limit: updatedQuery.builder.queryData[0].limit || 0,
},
// we do not need select columns in case of logs
selectColumns:
initialDataSource === DataSource.TRACES && widget.selectedTracesFields,
},
fillGaps: widget.fillSpans,
start: startTime,
end: endTime,
};
}, [widget, updatedQuery, startTime, endTime]);
const queryResponse = useGetQueryRange(
{
...requestData,
originalGraphType: widget?.panelTypes,
},
ENTITY_VERSION_V5,
{
queryKey: [
widget?.query,
widget?.panelTypes,
requestData,
startTime,
endTime,
],
retry(failureCount, error): boolean {
if (
String(error).includes('status: error') &&
String(error).includes('i/o timeout')
) {
return false;
}
return failureCount < 2;
},
keepPreviousData: true,
enabled: !!widget?.query,
refetchOnMount: false,
},
{},
{
isPublic: true,
widgetIndex: index,
publicDashboardId: dashboardId,
},
);
const isEmptyLayout = widget?.id === PANEL_TYPES.EMPTY_WIDGET;
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.BAR) {
const sortedSeriesData = getSortedSeriesData(
queryResponse.data?.payload.data.result,
);
queryResponse.data.payload.data.result = sortedSeriesData;
}
if (queryResponse.data && widget.panelTypes === PANEL_TYPES.PIE) {
const transformedData = populateMultipleResults(queryResponse?.data);
// eslint-disable-next-line no-param-reassign
queryResponse.data = transformedData;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const onDragSelect = useCallback((_start: number, _end: number): void => {
// Handle drag select if needed - no-op for public dashboards
}, []);
return (
<div
className="panel-container"
style={{ height: '100%', width: '100%' }}
ref={graphRef}
>
{isEmptyLayout ? (
<EmptyWidget />
) : (
<WidgetGraphComponent
widget={widget}
queryResponse={queryResponse}
errorMessage={undefined}
headerMenuList={[]}
isWarning={false}
isFetchingResponse={queryResponse.isFetching || queryResponse.isLoading}
onDragSelect={onDragSelect}
/>
)}
</div>
);
}
export default memo(Panel);

View File

@@ -0,0 +1,109 @@
.public-dashboard-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
.public-dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 8px 16px;
border-bottom: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
position: sticky;
top: 0;
z-index: 999;
.public-dashboard-header-left {
display: flex;
align-items: center;
gap: 16px;
width: 50%;
.brand-logo {
display: flex;
align-items: center;
gap: 8px;
width: 100px;
}
.public-dashboard-header-title {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
width: calc(100% - 100px);
.public-dashboard-header-title-text {
font-family: 'Work Sans', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 18px;
// ellipsis text
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
}
}
.public-dashboard-header-right {
display: flex;
align-items: center;
gap: 16px;
.datetime-section {
.time-range-select-dropdown {
width: 200px;
}
}
}
.brand-logo {
display: flex;
align-items: center;
gap: 8px;
}
.brand-logo-name {
font-family: 'Work Sans', sans-serif;
font-size: 14px;
font-style: normal;
font-weight: 500;
line-height: 18px;
}
.brand-logo-img {
height: 24px;
width: 24px;
}
}
.public-dashboard-content {
width: 100%;
height: 100%;
}
.fullscreen-grid-container {
margin: 0px 8px;
}
}
.lightMode {
.public-dashboard-container {
.public-dashboard-header {
background: var(--bg-vanilla-100);
border-bottom: 1px solid var(--bg-vanilla-300);
}
}
}

View File

@@ -0,0 +1,241 @@
import './PublicDashboardContainer.styles.scss';
import { Typography } from 'antd';
import cx from 'classnames';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { themeColors } from 'constants/theme';
import { Card, CardContainer } from 'container/GridCardLayout/styles';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/config';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import GetMinMax from 'lib/getMinMax';
import { useMemo, useState } from 'react';
import RGL, { WidthProvider } from 'react-grid-layout';
import { SuccessResponseV2 } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { PublicDashboardDataProps } from 'types/api/dashboard/public/get';
import Panel from './Panel';
const ReactGridLayoutComponent = WidthProvider(RGL);
const CUSTOM_TIME_REGEX = /^(\d+)([mhdw])$/;
const getStartTimeAndEndTimeFromTimeRange = (
timeRange: string,
): { startTime: number; endTime: number } => {
const isValidFormat = CUSTOM_TIME_REGEX.test(timeRange);
if (isValidFormat) {
const match = timeRange.match(CUSTOM_TIME_REGEX) as RegExpMatchArray;
const timeValue = parseInt(match[1] as string, 10);
const timeUnit = match[2] as string;
switch (timeUnit) {
case 'm':
return {
startTime: dayjs().subtract(timeValue, 'minutes').unix(),
endTime: dayjs().unix(),
};
case 'h':
return {
startTime: dayjs().subtract(timeValue, 'hours').unix(),
endTime: dayjs().unix(),
};
case 'd':
return {
startTime: dayjs().subtract(timeValue, 'days').unix(),
endTime: dayjs().unix(),
};
case 'w':
return {
startTime: dayjs().subtract(timeValue, 'weeks').unix(),
endTime: dayjs().unix(),
};
default:
return { startTime: dayjs().unix(), endTime: dayjs().unix() };
}
}
return {
startTime: dayjs().subtract(30, 'minutes').unix(),
endTime: dayjs().unix(),
};
};
function PublicDashboardContainer({
publicDashboardId,
publicDashboardData,
}: {
publicDashboardId: string;
publicDashboardData: SuccessResponseV2<PublicDashboardDataProps>;
}): JSX.Element {
const isDarkMode = useIsDarkMode();
const { dashboard, publicDashboard } = publicDashboardData?.data || {};
const { widgets } = dashboard?.data || {};
const [selectedTimeRangeLabel, setSelectedTimeRangeLabel] = useState<string>(
publicDashboard?.defaultTimeRange || '30m',
);
const [selectedTimeRange, setSelectedTimeRange] = useState<{
startTime: number;
endTime: number;
}>(
getStartTimeAndEndTimeFromTimeRange(
publicDashboard?.defaultTimeRange || '30m',
),
);
const isTimeRangeEnabled = publicDashboard?.timeRangeEnabled || false;
// Memoize dashboardLayout to prevent array recreation on every render
const dashboardLayout = useMemo(() => dashboard?.data?.layout || [], [
dashboard?.data?.layout,
]);
const currentPanelMap = useMemo(() => dashboard?.data?.panelMap || {}, [
dashboard?.data?.panelMap,
]);
const handleTimeChange = (
interval: Time | CustomTimeType,
dateTimeRange?: [number, number],
): void => {
if (dateTimeRange) {
setSelectedTimeRange({
startTime: Math.floor(dateTimeRange[0] / 1000),
endTime: Math.floor(dateTimeRange[1] / 1000),
});
} else if (interval !== 'custom') {
const { maxTime, minTime } = GetMinMax(interval);
setSelectedTimeRange({
startTime: Math.floor(minTime / 1000000000),
endTime: Math.floor(maxTime / 1000000000),
});
}
setSelectedTimeRangeLabel(interval as string);
};
return (
<div className="public-dashboard-container">
<div className="public-dashboard-header">
<div className="public-dashboard-header-left">
<div className="brand-logo">
<img
src="/Logos/signoz-brand-logo.svg"
alt="SigNoz"
className="brand-logo-img"
/>
<Typography className="brand-logo-name">SigNoz</Typography>
</div>
<div className="public-dashboard-header-title">
<Typography.Text className="public-dashboard-header-title-text">
{dashboard?.data?.title}
</Typography.Text>
</div>
</div>
{isTimeRangeEnabled && (
<div className="public-dashboard-header-right">
<div className="datetime-section">
<DateTimeSelectionV2
showAutoRefresh={false}
showRefreshText={false}
hideShareModal
onTimeChange={handleTimeChange}
defaultRelativeTime={publicDashboard?.defaultTimeRange as Time}
isModalTimeSelection
modalSelectedInterval={selectedTimeRangeLabel as Time}
disableUrlSync
showRecentlyUsed={false}
/>
</div>
</div>
)}
</div>
<div className="public-dashboard-content fullscreen-grid-container">
<ReactGridLayoutComponent
cols={12}
rowHeight={45}
autoSize
width={100}
useCSSTransforms
isDraggable={false}
isDroppable={false}
isResizable={false}
allowOverlap={false}
layout={dashboardLayout}
style={{ backgroundColor: isDarkMode ? '' : themeColors.snowWhite }}
>
{dashboardLayout?.map((layout) => {
const { i: id } = layout;
const currentWidget = (widgets || [])?.find((e) => e.id === id);
const currentWidgetIndex = (widgets || [])?.findIndex((e) => e.id === id);
if (currentWidget?.panelTypes === PANEL_GROUP_TYPES.ROW) {
const rowWidgetProperties = currentPanelMap[id] || {};
let { title } = currentWidget;
if (rowWidgetProperties.collapsed) {
const widgetCount = rowWidgetProperties.widgets?.length || 0;
const collapsedText = `(${widgetCount} widget${
widgetCount > 1 ? 's' : ''
})`;
title += ` ${collapsedText}`;
}
return (
<CardContainer
isDarkMode={isDarkMode}
className="row-card"
key={id}
data-grid={JSON.stringify(currentWidget)}
>
<div className={cx('row-panel')}>
<div style={{ display: 'flex', gap: '6px', alignItems: 'center' }}>
<Typography.Text className="section-title">{title}</Typography.Text>
</div>
</div>
</CardContainer>
);
}
return (
<CardContainer
isDarkMode={isDarkMode}
key={id}
data-grid={JSON.stringify(currentWidget)}
>
<Card
className="grid-item"
isDarkMode={isDarkMode}
$panelType={currentWidget?.panelTypes || PANEL_TYPES.TIME_SERIES}
>
<Panel
dashboardId={publicDashboardId}
widget={(currentWidget as Widgets) || ({ id, query: {} } as Widgets)}
index={currentWidgetIndex}
startTime={selectedTimeRange.startTime}
endTime={selectedTimeRange.endTime}
/>
</Card>
</CardContainer>
);
})}
</ReactGridLayoutComponent>
</div>
</div>
);
}
export default PublicDashboardContainer;

View File

@@ -0,0 +1,802 @@
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { StatusCodes } from 'http-status-codes';
import {
publicDashboardResponse,
publicDashboardWidgetData,
} from 'mocks-server/__mockdata__/publicDashboard';
import { rest, server } from 'mocks-server/server';
import { Layout } from 'react-grid-layout';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import { SuccessResponseV2 } from 'types/api';
import { Widgets } from 'types/api/dashboard/getAll';
import { PublicDashboardDataProps } from 'types/api/dashboard/public/get';
import { EQueryType } from 'types/common/dashboard';
import PublicDashboardContainer from '../PublicDashboardContainer';
// Mock dependencies
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: jest.fn(() => false),
}));
jest.mock('lib/getMinMax', () => ({
__esModule: true,
default: jest.fn((interval: string) => {
if (interval === '1h') {
return {
minTime: 1000000000000,
maxTime: 2000000000000,
};
}
return {
minTime: 500000000000,
maxTime: 1000000000000,
};
}),
}));
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
__esModule: true,
default: ({
onTimeChange,
}: {
onTimeChange: (interval: string, dateTimeRange?: [number, number]) => void;
}): JSX.Element => (
<div data-testid="datetime-selection">
<button
type="button"
onClick={(): void => onTimeChange('1h')}
aria-label="Change time to 1 hour"
>
Change Time
</button>
<button
type="button"
onClick={(): void => onTimeChange('custom', [1000000, 2000000])}
aria-label="Set custom time range"
>
Custom Time
</button>
</div>
),
}));
jest.mock('../Panel', () => ({
__esModule: true,
default: ({
widget,
startTime,
endTime,
}: {
widget: Widgets;
startTime: number;
endTime: number;
}): JSX.Element => (
<div data-testid={`panel-${widget.id}`}>
<span>
Panel: {widget.id} ({startTime}-{endTime})
</span>
</div>
),
}));
jest.mock('react-grid-layout', () => ({
__esModule: true,
default: ({
children,
layout,
style,
}: {
children: React.ReactNode;
layout: Layout[];
style?: React.CSSProperties;
}): JSX.Element => (
<div
data-testid="grid-layout"
data-layout={JSON.stringify(layout)}
style={style}
>
{children}
</div>
),
WidthProvider: (
Component: React.ComponentType<unknown>,
): React.ComponentType<unknown> => Component,
}));
// Mock dayjs
jest.mock('dayjs', () => {
const actualDayjs = jest.requireActual('dayjs');
const mockUnix = jest.fn(() => 1000);
const mockUtcOffset = jest.fn(() => 0);
const mockTzMethod = jest.fn(() => ({
utcOffset: mockUtcOffset,
}));
const mockSubtract = jest.fn(() => ({
subtract: jest.fn(),
unix: mockUnix,
tz: mockTzMethod,
}));
const mockDayjs = jest.fn(() => ({
subtract: mockSubtract,
unix: mockUnix,
tz: mockTzMethod,
}));
Object.keys(actualDayjs).forEach((key) => {
((mockDayjs as unknown) as Record<string, unknown>)[
key
] = (actualDayjs as Record<string, unknown>)[key];
});
((mockDayjs as unknown) as { extend: jest.Mock }).extend = jest.fn();
((mockDayjs as unknown) as { tz: { guess: jest.Mock } }).tz = {
guess: jest.fn(() => 'UTC'),
};
return mockDayjs;
});
const mockUseIsDarkMode = jest.mocked(useIsDarkMode);
// MSW setup
beforeAll(() => {
server.listen();
});
afterAll(() => {
server.close();
});
afterEach(() => {
server.resetHandlers();
});
// Test constants
const MOCK_PUBLIC_DASHBOARD_ID = 'test-dashboard-id';
const MOCK_PUBLIC_PATH = '/public/dashboard/test';
const DEFAULT_TIME_RANGE = '30m';
// Use title from mock data
const TEST_DASHBOARD_TITLE = publicDashboardResponse.data.dashboard.data.title;
// Use widget ID from mock data
const WIDGET_1_ID =
publicDashboardResponse.data.dashboard.data.widgets?.[0]?.id || 'widget-1';
const WIDGET_1_TITLE = 'Widget 1';
const ROW_PANEL_ID = 'row-1';
const ROW_PANEL_TITLE = 'Row Panel';
// Type definitions
interface MockWidget {
id: string;
panelTypes: PANEL_TYPES | PANEL_GROUP_TYPES;
title: string;
query?: Widgets['query'];
description?: string;
opacity?: string;
nullZeroValues?: string;
timePreferance?: string;
softMin?: number | null;
softMax?: number | null;
selectedLogFields?: null;
selectedTracesFields?: null;
}
interface MockPublicDashboardData {
dashboard: {
data: {
title: string;
widgets?: MockWidget[];
layout?: Layout[];
panelMap?: Record<string, { widgets: Layout[]; collapsed: boolean }>;
variables?: Record<string, unknown>;
};
};
publicDashboard: {
timeRangeEnabled: boolean;
defaultTimeRange: string;
publicPath: string;
};
}
// Helper function to create mock query
const createMockQuery = (): Widgets['query'] => ({
builder: {
queryData: [],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [],
promql: [],
id: 'query-1',
queryType: EQueryType.QUERY_BUILDER,
});
// Base mock data - transform publicDashboardResponse to match component's expected format
const baseMockData: SuccessResponseV2<PublicDashboardDataProps> = {
data: (publicDashboardResponse.data as unknown) as PublicDashboardDataProps,
httpStatusCode: StatusCodes.OK,
};
// Helper function to create mock data with optional overrides
const createMockData = (
overrides?: Partial<MockPublicDashboardData>,
): SuccessResponseV2<PublicDashboardDataProps> => {
if (!overrides) {
return baseMockData;
}
const baseData = baseMockData.data;
// Apply overrides if provided
const mergedData: PublicDashboardDataProps = {
dashboard:
(overrides?.dashboard as PublicDashboardDataProps['dashboard']) ||
baseData.dashboard,
publicDashboard: overrides?.publicDashboard || baseData.publicDashboard,
};
return {
data: mergedData,
httpStatusCode: StatusCodes.OK,
};
};
describe('Public Dashboard Container', () => {
beforeEach(() => {
jest.clearAllMocks();
mockUseIsDarkMode.mockReturnValue(false);
// Set up default MSW handler for widget query range API
server.use(
rest.get(
'*/public/dashboards/:dashboardId/widgets/:widgetIndex/query_range',
(_req, res, ctx) =>
res(ctx.status(StatusCodes.OK), ctx.json(publicDashboardWidgetData)),
),
);
});
describe('Rendering', () => {
it('should render dashboard with title and brand logo', () => {
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={baseMockData}
/>,
);
expect(screen.getByText(TEST_DASHBOARD_TITLE)).toBeInTheDocument();
expect(screen.getByText('SigNoz')).toBeInTheDocument();
expect(screen.getByAltText('SigNoz')).toBeInTheDocument();
});
it('should render time range selector when timeRangeEnabled is true', () => {
const mockData = createMockData({
publicDashboard: {
timeRangeEnabled: true,
defaultTimeRange: DEFAULT_TIME_RANGE,
publicPath: MOCK_PUBLIC_PATH,
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
expect(screen.getByTestId('datetime-selection')).toBeInTheDocument();
expect(
screen.getByRole('button', { name: /change time to 1 hour/i }),
).toBeInTheDocument();
});
it('should not render time range selector when timeRangeEnabled is false', () => {
const mockData = createMockData({
publicDashboard: {
timeRangeEnabled: false,
defaultTimeRange: DEFAULT_TIME_RANGE,
publicPath: MOCK_PUBLIC_PATH,
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
expect(screen.queryByTestId('datetime-selection')).not.toBeInTheDocument();
});
it('should render widgets in grid layout', () => {
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={baseMockData}
/>,
);
expect(screen.getByTestId('grid-layout')).toBeInTheDocument();
expect(screen.getByTestId(`panel-${WIDGET_1_ID}`)).toBeInTheDocument();
expect(
screen.getByText(new RegExp(`Panel: ${WIDGET_1_ID}`)),
).toBeInTheDocument();
});
it('should handle empty dashboard data gracefully', () => {
const mockData = createMockData({
dashboard: {
data: {
title: 'Empty Dashboard',
widgets: [],
layout: [],
panelMap: {},
variables: {},
},
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
expect(screen.getByText('Empty Dashboard')).toBeInTheDocument();
expect(screen.getByTestId('grid-layout')).toBeInTheDocument();
});
});
describe('Time Range Handling', () => {
it('should initialize with default time range from publicDashboard', () => {
const mockData = createMockData({
publicDashboard: {
timeRangeEnabled: true,
defaultTimeRange: '1h',
publicPath: MOCK_PUBLIC_PATH,
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
// Panel should receive the initial time range
expect(screen.getByTestId(`panel-${WIDGET_1_ID}`)).toBeInTheDocument();
});
it('should update time range when time change handler is called with interval', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockData = createMockData({
publicDashboard: {
timeRangeEnabled: true,
defaultTimeRange: DEFAULT_TIME_RANGE,
publicPath: MOCK_PUBLIC_PATH,
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
const timeChangeButton = screen.getByRole('button', {
name: /change time to 1 hour/i,
});
await user.click(timeChangeButton);
await waitFor(() => {
const panel = screen.getByTestId(`panel-${WIDGET_1_ID}`);
expect(panel).toBeInTheDocument();
});
});
it('should update time range when time change handler is called with custom dateTimeRange', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockData = createMockData({
publicDashboard: {
timeRangeEnabled: true,
defaultTimeRange: DEFAULT_TIME_RANGE,
publicPath: MOCK_PUBLIC_PATH,
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
const customTimeButton = screen.getByRole('button', {
name: /set custom time range/i,
});
await user.click(customTimeButton);
await waitFor(() => {
// Panel should receive updated time range
expect(screen.getByTestId(`panel-${WIDGET_1_ID}`)).toBeInTheDocument();
});
});
it('should use default time range of 30m when defaultTimeRange is not provided', () => {
const mockData = createMockData({
publicDashboard: {
timeRangeEnabled: true,
defaultTimeRange: (undefined as unknown) as string,
publicPath: MOCK_PUBLIC_PATH,
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
expect(screen.getByTestId(`panel-${WIDGET_1_ID}`)).toBeInTheDocument();
});
});
describe('Panel Rendering', () => {
it('should render row panel when widget panelTypes is ROW', () => {
const mockData = createMockData({
dashboard: {
data: {
title: TEST_DASHBOARD_TITLE,
widgets: [
{
id: ROW_PANEL_ID,
panelTypes: PANEL_GROUP_TYPES.ROW,
title: ROW_PANEL_TITLE,
},
],
layout: [
{
i: ROW_PANEL_ID,
x: 0,
y: 0,
w: 12,
h: 2,
},
],
panelMap: {},
variables: {},
},
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
expect(screen.getByText(ROW_PANEL_TITLE)).toBeInTheDocument();
});
it('should render collapsed row panel with widget count', () => {
const mockData = createMockData({
dashboard: {
data: {
title: TEST_DASHBOARD_TITLE,
widgets: [
{
id: ROW_PANEL_ID,
panelTypes: PANEL_GROUP_TYPES.ROW,
title: ROW_PANEL_TITLE,
},
],
layout: [
{
i: ROW_PANEL_ID,
x: 0,
y: 0,
w: 12,
h: 2,
},
],
panelMap: {
[ROW_PANEL_ID]: {
widgets: [
{ i: 'w1', x: 0, y: 0, w: 6, h: 6 },
{ i: 'w2', x: 6, y: 0, w: 6, h: 6 },
],
collapsed: true,
},
},
variables: {},
},
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
expect(screen.getByText(/Row Panel \(2 widgets\)/)).toBeInTheDocument();
});
it('should render collapsed row panel with singular widget count', () => {
const mockData = createMockData({
dashboard: {
data: {
title: TEST_DASHBOARD_TITLE,
widgets: [
{
id: ROW_PANEL_ID,
panelTypes: PANEL_GROUP_TYPES.ROW,
title: ROW_PANEL_TITLE,
},
],
layout: [
{
i: ROW_PANEL_ID,
x: 0,
y: 0,
w: 12,
h: 2,
},
],
panelMap: {
[ROW_PANEL_ID]: {
widgets: [{ i: 'w1', x: 0, y: 0, w: 6, h: 6 }],
collapsed: true,
},
},
variables: {},
},
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
expect(screen.getByText(/Row Panel \(1 widget\)/)).toBeInTheDocument();
});
it('should render regular panel for non-ROW widget types', () => {
const mockData = createMockData({
dashboard: {
data: {
title: TEST_DASHBOARD_TITLE,
widgets: [
{
id: WIDGET_1_ID,
panelTypes: PANEL_TYPES.TIME_SERIES,
title: WIDGET_1_TITLE,
query: createMockQuery(),
},
],
layout: [
{
i: WIDGET_1_ID,
x: 0,
y: 0,
w: 6,
h: 6,
},
],
panelMap: {},
variables: {},
},
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
expect(screen.getByTestId(`panel-${WIDGET_1_ID}`)).toBeInTheDocument();
expect(
screen.getByText(new RegExp(`Panel: ${WIDGET_1_ID}`)),
).toBeInTheDocument();
});
it('should handle missing widget in layout gracefully', () => {
const mockData = createMockData({
dashboard: {
data: {
title: TEST_DASHBOARD_TITLE,
widgets: [
{
id: WIDGET_1_ID,
panelTypes: PANEL_TYPES.TIME_SERIES,
title: WIDGET_1_TITLE,
query: createMockQuery(),
},
],
layout: [
{
i: 'missing-widget',
x: 0,
y: 0,
w: 6,
h: 6,
},
],
panelMap: {},
variables: {},
},
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
// Should render panel with fallback widget data
expect(screen.getByTestId('panel-missing-widget')).toBeInTheDocument();
expect(screen.getByText(/Panel: missing-widget/)).toBeInTheDocument();
});
});
describe('Dark Mode', () => {
it('should apply dark mode styles when isDarkMode is true', () => {
mockUseIsDarkMode.mockReturnValue(true);
const { container } = render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={baseMockData}
/>,
);
const gridLayout = container.querySelector('[data-testid="grid-layout"]');
expect(gridLayout).toBeInTheDocument();
if (gridLayout) {
expect(gridLayout).toHaveStyle({ backgroundColor: '' });
}
});
it('should apply light mode styles when isDarkMode is false', () => {
mockUseIsDarkMode.mockReturnValue(false);
const { container } = render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={baseMockData}
/>,
);
const gridLayout = container.querySelector('[data-testid="grid-layout"]');
expect(gridLayout).toBeInTheDocument();
if (gridLayout) {
// themeColors.snowWhite is '#fafafa' which computes to 'rgb(250, 250, 250)'
expect(gridLayout).toHaveStyle({
backgroundColor: 'rgb(250, 250, 250)',
});
}
});
});
describe('Edge Cases', () => {
it('should handle undefined dashboard data', () => {
const mockData: SuccessResponseV2<PublicDashboardDataProps> = {
data: {
dashboard: (undefined as unknown) as PublicDashboardDataProps['dashboard'],
publicDashboard: {
timeRangeEnabled: false,
defaultTimeRange: DEFAULT_TIME_RANGE,
publicPath: MOCK_PUBLIC_PATH,
},
},
httpStatusCode: StatusCodes.OK,
};
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
expect(screen.getByText('SigNoz')).toBeInTheDocument();
});
it('should handle missing layout data', () => {
const mockData = createMockData({
dashboard: {
data: {
title: TEST_DASHBOARD_TITLE,
widgets: [
{
id: WIDGET_1_ID,
panelTypes: PANEL_TYPES.TIME_SERIES,
title: WIDGET_1_TITLE,
query: createMockQuery(),
},
],
layout: (undefined as unknown) as Layout[],
panelMap: {},
variables: {},
},
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
// Component should render without errors even with missing layout
expect(screen.getByText(TEST_DASHBOARD_TITLE)).toBeInTheDocument();
});
it('should handle multiple widgets in layout', () => {
const mockData = createMockData({
dashboard: {
data: {
title: TEST_DASHBOARD_TITLE,
widgets: [
{
id: WIDGET_1_ID,
panelTypes: PANEL_TYPES.TIME_SERIES,
title: WIDGET_1_TITLE,
query: createMockQuery(),
},
{
id: 'widget-2',
panelTypes: PANEL_TYPES.TABLE,
title: 'Widget 2',
query: createMockQuery(),
},
],
layout: [
{
i: WIDGET_1_ID,
x: 0,
y: 0,
w: 6,
h: 6,
},
{
i: 'widget-2',
x: 6,
y: 0,
w: 6,
h: 6,
},
],
panelMap: {},
variables: {},
},
},
});
render(
<PublicDashboardContainer
publicDashboardId={MOCK_PUBLIC_DASHBOARD_ID}
publicDashboardData={mockData}
/>,
);
expect(screen.getByTestId(`panel-${WIDGET_1_ID}`)).toBeInTheDocument();
expect(screen.getByTestId('panel-widget-2')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,3 @@
import PublicDashboardContainer from './PublicDashboardContainer';
export default PublicDashboardContainer;

View File

@@ -1,6 +1,8 @@
/* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable react/jsx-props-no-spreading */
import { Button, Flex, Input, Select } from 'antd';
import { Button, Flex, Select } from 'antd';
import cx from 'classnames'; import cx from 'classnames';
import OverflowInputToolTip from 'components/OverflowInputToolTip';
import { import {
logsQueryFunctionOptions, logsQueryFunctionOptions,
metricQueryFunctionOptions, metricQueryFunctionOptions,
@@ -9,6 +11,7 @@ import {
import { useIsDarkMode } from 'hooks/useDarkMode'; import { useIsDarkMode } from 'hooks/useDarkMode';
import { debounce, isNil } from 'lodash-es'; import { debounce, isNil } from 'lodash-es';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { useMemo, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { QueryFunction } from 'types/api/v5/queryRange'; import { QueryFunction } from 'types/api/v5/queryRange';
import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder'; import { DataSource, QueryFunctionsTypes } from 'types/common/queryBuilder';
@@ -47,9 +50,13 @@ export default function Function({
functionValue = funcData.args?.[0]?.value; functionValue = funcData.args?.[0]?.value;
} }
const debouncedhandleUpdateFunctionArgs = debounce( const [value, setValue] = useState<string>(
handleUpdateFunctionArgs, functionValue !== undefined ? String(functionValue) : '',
500, );
const debouncedhandleUpdateFunctionArgs = useMemo(
() => debounce(handleUpdateFunctionArgs, 500),
[handleUpdateFunctionArgs],
); );
// update the logic when we start supporting functions for traces // update the logic when we start supporting functions for traces
@@ -89,13 +96,18 @@ export default function Function({
/> />
{showInput && ( {showInput && (
<Input <OverflowInputToolTip
className="query-function-value"
autoFocus autoFocus
defaultValue={functionValue} value={value}
onChange={(event): void => { onChange={(event): void => {
const newVal = event.target.value;
setValue(newVal);
debouncedhandleUpdateFunctionArgs(funcData, index, event.target.value); debouncedhandleUpdateFunctionArgs(funcData, index, event.target.value);
}} }}
tooltipPlacement="top"
minAutoWidth={70}
maxAutoWidth={150}
className="query-function-value"
/> />
)} )}

View File

@@ -99,7 +99,7 @@
} }
.query-function-value { .query-function-value {
width: 55px; width: 70px;
border-left: 0; border-left: 0;
background: var(--bg-ink-200); background: var(--bg-ink-200);
border-radius: 0; border-radius: 0;

View File

@@ -68,6 +68,7 @@ import { USER_ROLES } from 'types/roles';
import { checkVersionState } from 'utils/app'; import { checkVersionState } from 'utils/app';
import { showErrorNotification } from 'utils/error'; import { showErrorNotification } from 'utils/error';
import { useCmdK } from '../../providers/cmdKProvider';
import { routeConfig } from './config'; import { routeConfig } from './config';
import { getQueryString } from './helper'; import { getQueryString } from './helper';
import { import {
@@ -120,6 +121,7 @@ function SortableFilter({ item }: { item: SidebarItem }): JSX.Element {
// eslint-disable-next-line sonarjs/cognitive-complexity // eslint-disable-next-line sonarjs/cognitive-complexity
function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element { function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
const { openCmdK } = useCmdK();
const { pathname, search } = useLocation(); const { pathname, search } = useLocation();
const { currentVersion, latestVersion, isCurrentVersionError } = useSelector< const { currentVersion, latestVersion, isCurrentVersionError } = useSelector<
AppState, AppState,
@@ -637,6 +639,8 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
} else { } else {
history.push(settingsRoute); history.push(settingsRoute);
} }
} else if (item.key === 'quick-search') {
openCmdK();
} else if (item) { } else if (item) {
onClickHandler(item?.key as string, event); onClickHandler(item?.key as string, event);
} }

View File

@@ -26,6 +26,7 @@ import {
Receipt, Receipt,
Route, Route,
ScrollText, ScrollText,
Search,
Settings, Settings,
Slack, Slack,
Unplug, Unplug,
@@ -188,6 +189,12 @@ export const primaryMenuItems: SidebarItem[] = [
icon: <Home size={16} />, icon: <Home size={16} />,
itemKey: 'home', itemKey: 'home',
}, },
{
key: 'quick-search',
label: 'Search',
icon: <Search size={16} />,
itemKey: 'quick-search',
},
{ {
key: ROUTES.LIST_ALL_ALERT, key: ROUTES.LIST_ALL_ALERT,
label: 'Alerts', label: 'Alerts',

View File

@@ -8,13 +8,4 @@
min-height: 350px; min-height: 350px;
padding: 0px 12px; padding: 0px 12px;
} }
.time-series-view-container {
.time-series-view-container-header {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 12px 0;
}
}
} }

View File

@@ -11,7 +11,6 @@ import { LogsLoading } from 'container/LogsLoading/LogsLoading';
import EmptyMetricsSearch from 'container/MetricsExplorer/Explorer/EmptyMetricsSearch'; import EmptyMetricsSearch from 'container/MetricsExplorer/Explorer/EmptyMetricsSearch';
import { MetricsLoading } from 'container/MetricsExplorer/MetricsLoading/MetricsLoading'; import { MetricsLoading } from 'container/MetricsExplorer/MetricsLoading/MetricsLoading';
import NoLogs from 'container/NoLogs/NoLogs'; import NoLogs from 'container/NoLogs/NoLogs';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config'; import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/config';
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading'; import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
@@ -82,13 +81,6 @@ function TimeSeriesView({
const [minTimeScale, setMinTimeScale] = useState<number>(); const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>(); const [maxTimeScale, setMaxTimeScale] = useState<number>();
const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]); const [graphVisibility, setGraphVisibility] = useState<boolean[]>([]);
const [yAxisUnitInternal, setYAxisUnitInternal] = useState<string>(
yAxisUnit || '',
);
const onUnitChangeHandler = (value: string): void => {
setYAxisUnitInternal(value);
};
const legendScrollPositionRef = useRef<{ const legendScrollPositionRef = useRef<{
scrollTop: number; scrollTop: number;
@@ -198,7 +190,7 @@ function TimeSeriesView({
const chartOptions = getUPlotChartOptions({ const chartOptions = getUPlotChartOptions({
id: 'time-series-explorer', id: 'time-series-explorer',
onDragSelect, onDragSelect,
yAxisUnit: yAxisUnitInternal || '', yAxisUnit: yAxisUnit || '',
apiResponse: data?.payload, apiResponse: data?.payload,
dimensions: { dimensions: {
width: containerDimensions.width, width: containerDimensions.width,
@@ -270,17 +262,7 @@ function TimeSeriesView({
!isError && !isError &&
chartData && chartData &&
!isEmpty(chartData?.[0]) && !isEmpty(chartData?.[0]) &&
chartOptions && ( chartOptions && <Uplot data={chartData} options={chartOptions} />}
<div className="time-series-view-container">
<div className="time-series-view-container-header">
<BuilderUnitsFilter
onChange={onUnitChangeHandler}
yAxisUnit={yAxisUnitInternal}
/>
</div>
<Uplot data={chartData} options={chartOptions} />
</div>
)}
</div> </div>
</div> </div>
); );

View File

@@ -65,6 +65,8 @@ function DateTimeSelection({
onGoLive, onGoLive,
onExitLiveLogs, onExitLiveLogs,
showLiveLogs, showLiveLogs,
disableUrlSync = false,
showRecentlyUsed = true,
}: Props): JSX.Element { }: Props): JSX.Element {
const [formSelector] = Form.useForm(); const [formSelector] = Form.useForm();
const { safeNavigate } = useSafeNavigate(); const { safeNavigate } = useSafeNavigate();
@@ -563,6 +565,11 @@ function DateTimeSelection({
// this is triggred when we change the routes and based on that we are changing the default options // this is triggred when we change the routes and based on that we are changing the default options
useEffect(() => { useEffect(() => {
// Skip URL sync when disabled (e.g., public dashboards)
if (disableUrlSync) {
return;
}
const metricsTimeDuration = getLocalStorageKey( const metricsTimeDuration = getLocalStorageKey(
LOCALSTORAGE.METRICS_TIME_IN_DURATION, LOCALSTORAGE.METRICS_TIME_IN_DURATION,
); );
@@ -633,7 +640,7 @@ function DateTimeSelection({
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`; const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
safeNavigate(generatedUrl); safeNavigate(generatedUrl);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname, updateTimeInterval, globalTimeLoading]); }, [location.pathname, updateTimeInterval, globalTimeLoading, disableUrlSync]);
const { timezone } = useTimezone(); const { timezone } = useTimezone();
@@ -716,6 +723,7 @@ function DateTimeSelection({
customDateTimeVisible={customDateTimeVisible} customDateTimeVisible={customDateTimeVisible}
setCustomDTPickerVisible={setCustomDTPickerVisible} setCustomDTPickerVisible={setCustomDTPickerVisible}
onExitLiveLogs={onExitLiveLogs} onExitLiveLogs={onExitLiveLogs}
showRecentlyUsed={showRecentlyUsed}
/> />
{showAutoRefresh && selectedTime !== 'custom' && ( {showAutoRefresh && selectedTime !== 'custom' && (
@@ -756,6 +764,10 @@ interface DateTimeSelectionV2Props {
showLiveLogs?: boolean; showLiveLogs?: boolean;
onGoLive?: () => void; onGoLive?: () => void;
onExitLiveLogs?: () => void; onExitLiveLogs?: () => void;
/** When true, prevents the component from modifying URL parameters (useful for public dashboards or isolated contexts) */
disableUrlSync?: boolean;
/** When false, hides the "Recently Used" time ranges section in the time picker */
showRecentlyUsed?: boolean;
} }
DateTimeSelection.defaultProps = { DateTimeSelection.defaultProps = {
@@ -772,6 +784,8 @@ DateTimeSelection.defaultProps = {
onGoLive: (): void => {}, onGoLive: (): void => {},
onExitLiveLogs: (): void => {}, onExitLiveLogs: (): void => {},
showLiveLogs: false, showLiveLogs: false,
disableUrlSync: false,
showRecentlyUsed: true,
}; };
interface DispatchProps { interface DispatchProps {
updateTimeInterval: ( updateTimeInterval: (

View File

@@ -0,0 +1,18 @@
import getPublicDashboardDataAPI from 'api/dashboard/public/getPublicDashboardData';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery, UseQueryResult } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import { PublicDashboardDataProps } from 'types/api/dashboard/public/get';
import APIError from 'types/api/error';
export const useGetPublicDashboardData = (
id: string,
): UseQueryResult<SuccessResponseV2<PublicDashboardDataProps>, APIError> =>
useQuery<SuccessResponseV2<PublicDashboardDataProps>, APIError>({
queryFn: () => getPublicDashboardDataAPI({ id }),
onError: (error) => {
console.error('Error getting public dashboard data', error);
},
queryKey: [REACT_QUERY_KEY.GET_PUBLIC_DASHBOARD, id],
enabled: !!id,
});

View File

@@ -0,0 +1,19 @@
import getPublicDashboardMetaAPI from 'api/dashboard/public/getPublicDashboardMeta';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useQuery, UseQueryResult } from 'react-query';
import { SuccessResponseV2 } from 'types/api';
import { PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
import APIError from 'types/api/error';
export const useGetPublicDashboardMeta = (
id: string,
): UseQueryResult<SuccessResponseV2<PublicDashboardMetaProps>, APIError> =>
useQuery<SuccessResponseV2<PublicDashboardMetaProps>, APIError>({
queryFn: () => getPublicDashboardMetaAPI({ id }),
onError: (error) => {
console.error('Error getting public dashboard', error);
},
queryKey: [REACT_QUERY_KEY.GET_PUBLIC_DASHBOARD_META, id],
enabled: !!id,
keepPreviousData: false,
});

View File

@@ -26,6 +26,11 @@ type UseGetQueryRange = (
version: string, version: string,
options?: UseGetQueryRangeOptions, options?: UseGetQueryRangeOptions,
headers?: Record<string, string>, headers?: Record<string, string>,
publicQueryMeta?: {
isPublic: boolean;
widgetIndex: number;
publicDashboardId: string;
},
) => UseQueryResult< ) => UseQueryResult<
SuccessResponse<MetricRangePayloadProps> & { warning?: Warning }, SuccessResponse<MetricRangePayloadProps> & { warning?: Warning },
Error Error
@@ -36,6 +41,7 @@ export const useGetQueryRange: UseGetQueryRange = (
version, version,
options, options,
headers, headers,
publicQueryMeta,
) => { ) => {
const { selectedDashboard } = useDashboard(); const { selectedDashboard } = useDashboard();
@@ -156,6 +162,8 @@ export const useGetQueryRange: UseGetQueryRange = (
dynamicVariables, dynamicVariables,
signal, signal,
headers, headers,
undefined,
publicQueryMeta,
), ),
...options, ...options,
retry, retry,

View File

@@ -29,6 +29,7 @@ import { QueryData } from 'types/api/widgets/getQuery';
import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5'; import { createAggregation } from 'api/v5/queryRange/prepareQueryRangePayloadV5';
import { IDashboardVariable } from 'types/api/dashboard/getAll'; import { IDashboardVariable } from 'types/api/dashboard/getAll';
import { EQueryType } from 'types/common/dashboard'; import { EQueryType } from 'types/common/dashboard';
import getPublicDashboardWidgetData from 'api/dashboard/public/getPublicDashboardWidgetData';
/** /**
* Validates if metric name is available for METRICS data source * Validates if metric name is available for METRICS data source
@@ -200,6 +201,11 @@ export async function GetMetricQueryRange(
signal?: AbortSignal, signal?: AbortSignal,
headers?: Record<string, string>, headers?: Record<string, string>,
isInfraMonitoring?: boolean, isInfraMonitoring?: boolean,
publicQueryMeta?: {
isPublic: boolean;
widgetIndex: number;
publicDashboardId: string;
},
): Promise<SuccessResponse<MetricRangePayloadProps> & { warning?: Warning }> { ): Promise<SuccessResponse<MetricRangePayloadProps> & { warning?: Warning }> {
let legendMap: Record<string, string>; let legendMap: Record<string, string>;
let response: let response:
@@ -249,7 +255,10 @@ export async function GetMetricQueryRange(
legendMap = v5Result.legendMap; legendMap = v5Result.legendMap;
// atleast one query should be there to make call to v5 api // atleast one query should be there to make call to v5 api
if (v5Result.queryPayload.compositeQuery.queries.length === 0) { if (
v5Result.queryPayload.compositeQuery.queries.length === 0 &&
!publicQueryMeta?.isPublic
) {
return { return {
statusCode: 200, statusCode: 200,
error: null, error: null,
@@ -272,6 +281,26 @@ export async function GetMetricQueryRange(
}; };
} }
if (publicQueryMeta?.isPublic) {
const publicResponse = await getPublicDashboardWidgetData({
id: publicQueryMeta?.publicDashboardId,
index: publicQueryMeta?.widgetIndex,
startTime: props.start * 1000,
endTime: props.end * 1000,
});
// Convert V5 response to legacy format for components
response = convertV5ResponseToLegacy(
{
payload: publicResponse.data,
params: v5Result.queryPayload,
},
legendMap,
finalFormatForWeb,
);
warning = response.payload.warning || undefined;
} else {
const v5Response = await getQueryRangeV5( const v5Response = await getQueryRangeV5(
v5Result.queryPayload, v5Result.queryPayload,
version, version,
@@ -290,6 +319,7 @@ export async function GetMetricQueryRange(
); );
warning = response.payload.warning || undefined; warning = response.payload.warning || undefined;
}
} else { } else {
const legacyResult = prepareQueryRangePayload(props); const legacyResult = prepareQueryRangePayload(props);
legendMap = legacyResult.legendMap; legendMap = legacyResult.legendMap;

View File

@@ -0,0 +1,328 @@
export const publishedPublicDashboardMeta = {
status: 'success',
data: {
timeRangeEnabled: true,
defaultTimeRange: '30m',
publicPath: '/public/dashboard/019ac98e-383f-7e9f-b716-d15bcb6be4bb',
},
};
export const unpublishedPublicDashboardMeta = {
status: 'error',
error: {
code: 'public_dashboard_not_found',
message:
"dashboard with id 019ac4e8-1a35-718e-aa4b-0c9781d7a31c isn't public",
},
};
export const publicDashboardResponse = {
status: 'success',
data: {
dashboard: {
createdAt: '0001-01-01T00:00:00Z',
updatedAt: '0001-01-01T00:00:00Z',
createdBy: '',
updatedBy: '',
id: '',
data: {
description: '',
image:
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTgiIHZpZXdCb3g9IjAgMCAxOCAxOCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0Ljk0OTQgMTMuOTQyQzE2LjIzMTggMTIuNDI1OCAxNy4zMjY4IDkuNzAyMiAxNi4xOTU2IDYuNTc0ODdDMTUuNjQ0MyA1LjA1MjQ1IDE1LjAyMTkgNC4yMDI0OSAxNC4yOTY5IDMuNjYyNTJDMTMuODU1NyAzLjMzMzc5IDEyLjA5MzMgMi41MDYzMyA5Ljc1OTY1IDIuODY3NTZDOC4wNTM0OSAzLjEzMjU1IDUuNzc0ODcgNC4yMDg3NCA0LjI5MzY5IDUuOTU5OUMyLjg1NzUyIDcuNjYxMDYgMS43NDg4MyA5LjAwNDc0IDEuNjk3NTggMTAuMzA5N0MxLjYzMTMzIDExLjk4ODMgMi44OTYyNyAxMy40MzA4IDMuMDUwMDEgMTMuNjY0NUMzLjMyMzc0IDE0LjA3OTUgNS4xOTExNSAxNi40NTE4IDguNjk5NzEgMTYuNTczMUMxMS43OTcgMTYuNjc5MyAxMy44MTQ0IDE1LjI4NDQgMTQuOTQ5NCAxMy45NDJaIiBmaWxsPSIjNDAzRDNFIi8+CjxwYXRoIGQ9Ik00LjU1MzYzIDIuNzM3NDdDMi45Mzc0NiAzLjg5MTE2IDEuMTIxMzEgNi4yNTEwMyAxLjQ0NzU0IDkuNTYwODZDMS42MDYyOCAxMS4xNzIgMi4wMDI1MSAxMi4xNDk1IDIuNTcxMjMgMTIuODUwN0MyLjkxNzQ2IDEzLjI3ODIgNC40MTk4OCAxNC41NDkzIDYuNzczNTEgMTQuNzM2OEM5LjE0NTg4IDE0LjkyNTYgMTAuOTQ5NSAxNC4zOTQ0IDEyLjgzMzIgMTMuMDg0NEMxNi42NjE3IDEwLjQyMDggMTYuMDk4IDYuMzkzNTMgMTUuOTM0MyA1LjkyNDhDMTUuNzcwNSA1LjQ1NjA3IDE0LjU0NDQgMi42OTYyMiAxMS4xNzMzIDEuNzE1MDJDOC4xOTg0NCAwLjg1MDA2OCA1Ljk4MzU1IDEuNzE1MDIgNC41NTM2MyAyLjczNzQ3WiIgZmlsbD0iIzVFNjM2NyIvPgo8cGF0aCBkPSJNNy4zOTM1MyAyLjk2MTA5QzUuNjE3MzcgMi44OTczNCAzLjkxOTk2IDQuMjg4NTIgMy43NTYyMiA2LjAwNTkzQzMuNTkyNDggNy43MjIwOSA0LjY1NDkyIDkuMDI5NTIgNi4zMDk4MyA5LjI5NTc2QzcuOTY0NzUgOS41NjA3NCA5Ljg3ODM5IDguNTU1OCAxMC4yNjM0IDYuNDUwOTFDMTAuNjYwOSA0LjI4MjI3IDkuMDg5NjkgMy4wMjIzNCA3LjM5MzUzIDIuOTYxMDlaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBkPSJNNy45NDIxNyA1LjkwMTE1QzcuOTQyMTcgNS45MDExNSA4LjM2OTY1IDUuODEyNCA4LjQ1NDY1IDUuMTgyNDRDOC41MzgzOSA0LjU2MjQ3IDguMjMwOTEgNC4wMzM3NSA3LjUxMzQ1IDMuODQzNzZDNi43MzM0OSAzLjYzNzUyIDYuMjA0NzcgNC4wNjYyNSA2LjA2NzI3IDQuNTE3NDdDNS44NzYwMyA1LjE0NDk0IDYuMTU4NTIgNS40NDM2NyA2LjE1ODUyIDUuNDQzNjdDNi4xNTg1MiA1LjQ0MzY3IDUuMzkzNTYgNS42Mjc0MSA1LjMzMjMxIDYuNTI5ODdDNS4yNzQ4MSA3LjM4MTA3IDUuODU2MDMgNy44Mzg1NSA2LjQzOTc1IDcuOTc4NTRDNy4xNjA5NiA4LjE1MjI4IDcuOTc4NDIgNy45NTQ3OSA4LjE3ODQxIDcuMDM0ODRDOC4zNDQ2NSA2LjI3NzM4IDcuOTQyMTcgNS45MDExNSA3Ljk0MjE3IDUuOTAxMTVaIiBmaWxsPSIjMzAzMDMwIi8+CjxwYXRoIGQ9Ik02LjczOTgzIDQuNzUzNjJDNi42NzEwOSA1LjAxMjM1IDYuODA4NTggNS4yNjIzNCA3LjA3ODU3IDUuMzMxMDlDNy4zNjk4IDUuNDA0ODMgNy42MzQ3OSA1LjMwODU5IDcuNzA2MDMgNS4wMTExQzcuNzY4NTMgNC43NDczNyA3LjY0MzU0IDQuNTE0ODggNy4zMzYwNSA0LjQzOTg4QzcuMDgzNTcgNC4zNzczOSA2LjgxNDgzIDQuNDcxMTMgNi43Mzk4MyA0Ljc1MzYyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTYuOTU5NzggNi4wMzk3NEM2LjYzMjMgNS45Mzg0OSA2LjE5OTgyIDYuMDY0NzMgNi4xMzEwNyA2LjUwNDcxQzYuMDYyMzMgNi45NDQ2OSA2LjMyNjA2IDcuMTY5NjggNi42NzEwNCA3LjIzMjE3QzcuMDE2MDMgNy4yOTQ2NyA3LjM0MjI2IDcuMTEzNDMgNy40MDYwMSA2Ljc2MDk1QzcuNDY4NSA2LjQwOTcyIDcuMjg2MDEgNi4xMzk3MyA2Ljk1OTc4IDYuMDM5NzRaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K',
layout: [
{
h: 6,
i: '86590b60-8232-4b41-9744-32cadf2c95fb',
moved: false,
static: false,
w: 6,
x: 0,
y: 0,
},
],
panelMap: {},
tags: [],
title: 'Public Dashboard 02',
uploadedGrafana: false,
version: 'v5',
widgets: [
{
bucketCount: 30,
bucketWidth: 0,
columnUnits: {},
contextLinks: {
linksData: [],
},
customLegendColors: {},
decimalPrecision: 2,
description: '',
fillSpans: false,
id: '86590b60-8232-4b41-9744-32cadf2c95fb',
isLogScale: false,
legendPosition: 'bottom',
mergeAllActiveQueries: false,
nullZeroValues: 'zero',
opacity: '1',
panelTypes: 'graph',
query: {
builder: {
queryData: [
{
aggregations: [
{
metricName: 'container.cpu.time',
reduceTo: 'avg',
spaceAggregation: 'sum',
temporality: '',
timeAggregation: 'rate',
},
],
dataSource: 'metrics',
expression: 'A',
groupBy: [],
legend: '',
queryName: 'A',
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
legend: '',
name: 'A',
},
],
promql: [
{
legend: '',
name: 'A',
},
],
queryType: 'builder',
},
selectedLogFields: [
{
dataType: '',
fieldContext: 'log',
fieldDataType: '',
isIndexed: false,
name: 'timestamp',
signal: 'logs',
type: 'log',
},
{
dataType: '',
fieldContext: 'log',
fieldDataType: '',
isIndexed: false,
name: 'body',
signal: 'logs',
type: 'log',
},
],
selectedTracesFields: [
{
fieldContext: 'resource',
fieldDataType: 'string',
name: 'service.name',
signal: 'traces',
},
{
fieldContext: 'span',
fieldDataType: 'string',
name: 'name',
signal: 'traces',
},
{
fieldContext: 'span',
fieldDataType: '',
name: 'duration_nano',
signal: 'traces',
},
{
fieldContext: 'span',
fieldDataType: '',
name: 'http_method',
signal: 'traces',
},
{
fieldContext: 'span',
fieldDataType: '',
name: 'response_status_code',
signal: 'traces',
},
],
softMax: 0,
softMin: 0,
stackedBarChart: false,
thresholds: [],
timePreferance: 'GLOBAL_TIME',
title: '',
yAxisUnit: 'none',
},
],
},
locked: false,
org_id: '00000000-0000-0000-0000-000000000000',
},
publicDashboard: {
timeRangeEnabled: true,
defaultTimeRange: '30m',
publicPath: '/public/dashboard/019ad04e-8591-7013-879b-a2af376e4708',
},
},
};
export const publicDashboardWidgetData = {
status: 'success',
data: {
type: 'time_series',
meta: {
rowsScanned: 367490,
bytesScanned: 2977076,
durationMs: 330,
},
data: {
results: [
{
queryName: 'A',
aggregations: [
{
index: 0,
alias: '__result_0',
meta: {},
series: [
{
values: [
{
timestamp: 1764429600000,
partial: true,
value: 1.554,
},
{
timestamp: 1764429660000,
value: 1.367,
},
{
timestamp: 1764429720000,
value: 1.641,
},
{
timestamp: 1764429780000,
value: 1.455,
},
{
timestamp: 1764429840000,
value: 1.739,
},
{
timestamp: 1764429900000,
value: 1.318,
},
{
timestamp: 1764429960000,
value: 1.813,
},
{
timestamp: 1764430020000,
value: 1.332,
},
{
timestamp: 1764430080000,
value: 1.521,
},
{
timestamp: 1764430140000,
value: 1.461,
},
{
timestamp: 1764430200000,
value: 1.61,
},
{
timestamp: 1764430260000,
value: 1.747,
},
{
timestamp: 1764430320000,
value: 1.51,
},
{
timestamp: 1764430380000,
value: 1.501,
},
{
timestamp: 1764430440000,
value: 1.524,
},
{
timestamp: 1764430500000,
value: 1.648,
},
{
timestamp: 1764430560000,
value: 1.545,
},
{
timestamp: 1764430620000,
value: 1.47,
},
{
timestamp: 1764430680000,
value: 1.582,
},
{
timestamp: 1764430740000,
value: 1.257,
},
{
timestamp: 1764430800000,
value: 1.726,
},
{
timestamp: 1764430860000,
value: 1.482,
},
{
timestamp: 1764430920000,
value: 1.291,
},
{
timestamp: 1764430980000,
value: 1.741,
},
{
timestamp: 1764431040000,
value: 1.591,
},
{
timestamp: 1764431100000,
value: 1.609,
},
{
timestamp: 1764431160000,
value: 1.177,
},
{
timestamp: 1764431220000,
value: 1.699,
},
{
timestamp: 1764431280000,
value: 1.617,
},
{
timestamp: 1764431340000,
value: 1.515,
},
],
},
],
},
],
},
],
},
},
};

View File

@@ -1,3 +1,13 @@
import AlertRuleProvider from 'providers/Alert';
import AlertDetails from './AlertDetails'; import AlertDetails from './AlertDetails';
export default AlertDetails; function AlertDetailsPage(): JSX.Element {
return (
<AlertRuleProvider>
<AlertDetails />
</AlertRuleProvider>
);
}
export default AlertDetailsPage;

View File

@@ -0,0 +1,83 @@
.public-dashboard-page {
.public-dashboard-error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
padding: 64px 0px;
.public-dashboard-error-content-header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.brand {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 12px;
.brand-logo {
width: 40px;
height: 40px;
}
.brand-title {
font-size: 20px;
font-weight: 600;
color: var(--bg-vanilla-100, #ffffff); // White text for dark theme
margin: 0;
}
}
.brand-tagline {
margin-bottom: 24px;
.ant-typography {
color: var(--bg-vanilla-400, #c0c1c3); // Light gray text for dark theme
}
}
}
.public-dashboard-error-content {
margin: 128px 0px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 32px 0px;
gap: 8px;
.ant-typography {
margin: 0;
}
.public-dashboard-error-message {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 12px;
}
.public-dashboard-error-message-icon {
color: var(--bg-vanilla-400, #c0c1c3); // Light gray text for dark theme
}
}
}
}
.lightMode {
.public-dashboard-error-container {
.public-dashboard-error-content-header {
.brand-title {
color: var(--bg-ink-400, #121317) !important; // Dark text for light theme
}
}
}
}

View File

@@ -0,0 +1,80 @@
import './PublicDashboard.styles.scss';
import { Typography } from 'antd';
import { useGetPublicDashboardData } from 'hooks/dashboard/useGetPublicDashboardData';
import { FrownIcon } from 'lucide-react';
import { useParams } from 'react-router-dom';
import PublicDashboardContainer from '../../container/PublicDashboardContainer';
function PublicDashboardPage(): JSX.Element {
// read the dashboard id from the url
const { dashboardId } = useParams<{ dashboardId: string }>();
const {
data: publicDashboardData,
isLoading: isLoadingPublicDashboardData,
isFetching: isFetchingPublicDashboardData,
isError: isErrorPublicDashboardData,
} = useGetPublicDashboardData(dashboardId || '');
const isLoading =
isLoadingPublicDashboardData || isFetchingPublicDashboardData;
const isError = isErrorPublicDashboardData;
return (
<div className="public-dashboard-page">
{publicDashboardData && (
<PublicDashboardContainer
publicDashboardId={dashboardId}
publicDashboardData={publicDashboardData}
/>
)}
{isError && !isLoading && (
<div className="public-dashboard-error-container">
<div className="perilin-bg" />
<div className="public-dashboard-error-content-header">
<div className="brand">
<img
src="/Logos/signoz-brand-logo.svg"
alt="SigNoz"
className="brand-logo"
/>
<Typography.Title level={2} className="brand-title">
SigNoz
</Typography.Title>
</div>
<div className="brand-tagline">
<Typography.Text>
OpenTelemetry-Native Logs, Metrics and Traces in a single pane
</Typography.Text>
</div>
</div>
<div className="public-dashboard-error-content">
<Typography.Title
level={4}
className="public-dashboard-error-message-icon"
>
<FrownIcon size={36} />
</Typography.Title>
<Typography.Title level={4} className="public-dashboard-error-message">
The public dashboard you are looking for does not exist or has been
unpublished.
</Typography.Title>
<Typography.Text className="public-dashboard-error-message-description">
Please reach out to the owner of the dashboard to get access.
</Typography.Text>
</div>
</div>
)}
</div>
);
}
export default PublicDashboardPage;

View File

@@ -0,0 +1,8 @@
.trace-explorer-time-series-view-container {
&-header {
display: flex;
justify-content: flex-start;
align-items: center;
padding: 12px;
}
}

View File

@@ -3,6 +3,9 @@ import './TimeSeriesView.styles.scss';
import { ENTITY_VERSION_V5 } from 'constants/app'; import { ENTITY_VERSION_V5 } from 'constants/app';
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters';
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { import {
@@ -11,6 +14,7 @@ import {
SetStateAction, SetStateAction,
useEffect, useEffect,
useMemo, useMemo,
useState,
} from 'react'; } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
@@ -19,9 +23,6 @@ import APIError from 'types/api/error';
import { DataSource } from 'types/common/queryBuilder'; import { DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import TimeSeriesView from './TimeSeriesView';
import { convertDataValueToMs } from './utils';
function TimeSeriesViewContainer({ function TimeSeriesViewContainer({
dataSource = DataSource.TRACES, dataSource = DataSource.TRACES,
isFilterApplied, isFilterApplied,
@@ -31,11 +32,6 @@ function TimeSeriesViewContainer({
}: TimeSeriesViewProps): JSX.Element { }: TimeSeriesViewProps): JSX.Element {
const { stagedQuery, currentQuery, panelType } = useQueryBuilder(); const { stagedQuery, currentQuery, panelType } = useQueryBuilder();
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const isValidToConvertToMs = useMemo(() => { const isValidToConvertToMs = useMemo(() => {
const isValid: boolean[] = []; const isValid: boolean[] = [];
@@ -55,6 +51,19 @@ function TimeSeriesViewContainer({
return isValid.every(Boolean); return isValid.every(Boolean);
}, [currentQuery]); }, [currentQuery]);
const [yAxisUnit, setYAxisUnit] = useState<string>(
isValidToConvertToMs ? 'ms' : 'short',
);
const onUnitChangeHandler = (value: string): void => {
setYAxisUnit(value);
};
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
AppState,
GlobalReducer
>((state) => state.globalTime);
const queryKey = useMemo( const queryKey = useMemo(
() => [ () => [
REACT_QUERY_KEY.GET_QUERY_RANGE, REACT_QUERY_KEY.GET_QUERY_RANGE,
@@ -110,16 +119,21 @@ function TimeSeriesViewContainer({
}, [isLoading, isFetching, setIsLoadingQueries]); }, [isLoading, isFetching, setIsLoadingQueries]);
return ( return (
<div className="trace-explorer-time-series-view-container">
<div className="trace-explorer-time-series-view-container-header">
<BuilderUnitsFilter onChange={onUnitChangeHandler} yAxisUnit={yAxisUnit} />
</div>
<TimeSeriesView <TimeSeriesView
isFilterApplied={isFilterApplied} isFilterApplied={isFilterApplied}
isError={isError} isError={isError}
error={error as APIError} error={error as APIError}
isLoading={isLoading || isFetching} isLoading={isLoading || isFetching}
data={responseData} data={responseData}
yAxisUnit={isValidToConvertToMs ? 'ms' : 'short'} yAxisUnit={yAxisUnit}
dataSource={dataSource} dataSource={dataSource}
setWarning={setWarning} setWarning={setWarning}
/> />
</div>
); );
} }

View File

@@ -15,7 +15,6 @@ import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapp
import { useOptionsMenu } from 'container/OptionsMenu'; import { useOptionsMenu } from 'container/OptionsMenu';
import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions'; import LeftToolbarActions from 'container/QueryBuilder/components/ToolbarActions/LeftToolbarActions';
import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions'; import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions';
import TimeSeriesView from 'container/TimeSeriesView';
import Toolbar from 'container/Toolbar/Toolbar'; import Toolbar from 'container/Toolbar/Toolbar';
import { import {
getExportQueryData, getExportQueryData,
@@ -51,6 +50,8 @@ import {
} from 'utils/explorerUtils'; } from 'utils/explorerUtils';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import TimeSeriesView from './TimeSeriesView';
function TracesExplorer(): JSX.Element { function TracesExplorer(): JSX.Element {
const { const {
panelType, panelType,

View File

@@ -93,11 +93,24 @@
} }
&.danger { &.danger {
color: var(--bg-cherry-500) !important;
border-radius: 2px; border-radius: 2px;
border: 1px solid rgba(255, 184, 0, 0.1); color: var(--text-vanilla-100) !important;
background: rgba(255, 184, 0, 0.1) !important; text-align: center;
font-variant-numeric: slashed-zero;
/* Label/Small/500 */
font-family: Inter;
font-size: 11px;
font-style: normal;
font-weight: 500;
line-height: 100%; /* 11px */
background: var(--bg-cherry-500);
box-shadow: none !important; box-shadow: none !important;
&:hover {
border: 1px solid var(--bg-cherry-600) !important;
}
} }
} }

View File

@@ -1,229 +0,0 @@
import setLocalStorageApi from 'api/browser/localstorage/set';
import logEvent from 'api/common/logEvent';
import updateUserPreference from 'api/v1/user/preferences/name/update';
import { AxiosError } from 'axios';
import ROUTES from 'constants/routes';
import { USER_PREFERENCES } from 'constants/userPreferences';
import { useThemeMode } from 'hooks/useDarkMode';
import { THEME_MODE } from 'hooks/useDarkMode/constant';
import { useNotifications } from 'hooks/useNotifications';
import { KBarProvider } from 'kbar';
import history from 'lib/history';
import { useCallback } from 'react';
import { useMutation } from 'react-query';
import { UserPreference } from 'types/api/preferences/preference';
import { showErrorNotification } from 'utils/error';
import { useAppContext } from './App/App';
export function KBarCommandPaletteProvider({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
const { notifications } = useNotifications();
const { setAutoSwitch, setTheme } = useThemeMode();
const handleThemeChange = (value: string): void => {
logEvent('Account Settings: Theme Changed', {
theme: value,
});
if (value === 'auto') {
setAutoSwitch(true);
} else {
setAutoSwitch(false);
setTheme(value);
}
};
const onClickHandler = useCallback((key: string): void => {
history.push(key);
}, []);
const { updateUserPreferenceInContext } = useAppContext();
const { mutate: updateUserPreferenceMutation } = useMutation(
updateUserPreference,
{
onError: (error) => {
showErrorNotification(notifications, error as AxiosError);
},
},
);
const handleOpenSidebar = useCallback((): void => {
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, 'true');
// Update the context immediately
const save = {
name: USER_PREFERENCES.SIDENAV_PINNED,
value: true,
};
updateUserPreferenceInContext(save as UserPreference);
// Make the API call in the background
updateUserPreferenceMutation({
name: USER_PREFERENCES.SIDENAV_PINNED,
value: true,
});
}, [updateUserPreferenceInContext, updateUserPreferenceMutation]);
const handleCloseSidebar = useCallback((): void => {
setLocalStorageApi(USER_PREFERENCES.SIDENAV_PINNED, 'false');
// Update the context immediately
const save = {
name: USER_PREFERENCES.SIDENAV_PINNED,
value: false,
};
updateUserPreferenceInContext(save as UserPreference);
// Make the API call in the background
updateUserPreferenceMutation({
name: USER_PREFERENCES.SIDENAV_PINNED,
value: false,
});
}, [updateUserPreferenceInContext, updateUserPreferenceMutation]);
const kbarActions = [
{
id: 'home',
name: 'Go to Home',
shortcut: ['shift + h'],
keywords: 'home',
section: 'Navigation',
perform: (): void => {
onClickHandler(ROUTES.HOME);
},
},
{
id: 'dashboards',
name: 'Go to Dashboards',
shortcut: ['shift + d'],
keywords: 'dashboards',
section: 'Navigation',
perform: (): void => {
onClickHandler(ROUTES.ALL_DASHBOARD);
},
},
{
id: 'services',
name: 'Go to Services',
shortcut: ['shift + s'],
keywords: 'services monitoring',
section: 'Navigation',
perform: (): void => {
onClickHandler(ROUTES.APPLICATION);
},
},
{
id: 'traces',
name: 'Go to Traces',
shortcut: ['shift + t'],
keywords: 'traces',
section: 'Navigation',
perform: (): void => {
onClickHandler(ROUTES.TRACES_EXPLORER);
},
},
{
id: 'logs',
name: 'Go to Logs',
shortcut: ['shift + l'],
keywords: 'logs',
section: 'Navigation',
perform: (): void => {
onClickHandler(ROUTES.LOGS);
},
},
{
id: 'alerts',
name: 'Go to Alerts',
shortcut: ['shift + a'],
keywords: 'alerts',
section: 'Navigation',
perform: (): void => {
onClickHandler(ROUTES.LIST_ALL_ALERT);
},
},
{
id: 'exceptions',
name: 'Go to Exceptions',
shortcut: ['shift + e'],
keywords: 'exceptions errors',
section: 'Navigation',
perform: (): void => {
onClickHandler(ROUTES.ALL_ERROR);
},
},
{
id: 'messaging-queues',
name: 'Go to Messaging Queues',
shortcut: ['shift + m'],
keywords: 'messaging queues mq',
section: 'Navigation',
perform: (): void => {
onClickHandler(ROUTES.MESSAGING_QUEUES_OVERVIEW);
},
},
{
id: 'my-settings',
name: 'Go to Account Settings',
keywords: 'account settings',
section: 'Navigation',
perform: (): void => {
onClickHandler(ROUTES.MY_SETTINGS);
},
},
{
id: 'open-sidebar',
name: 'Open Sidebar',
keywords: 'sidebar navigation menu expand',
section: 'Settings',
perform: (): void => {
handleOpenSidebar();
},
},
{
id: 'collapse-sidebar',
name: 'Collapse Sidebar',
keywords: 'sidebar navigation menu collapse',
section: 'Settings',
perform: (): void => {
handleCloseSidebar();
},
},
{
id: 'dark-mode',
name: 'Switch to Dark Mode',
keywords: 'theme dark mode appearance',
section: 'Settings',
perform: (): void => {
handleThemeChange(THEME_MODE.DARK);
},
},
{
id: 'light-mode',
name: 'Switch to Light Mode [Beta]',
keywords: 'theme light mode appearance',
section: 'Settings',
perform: (): void => {
handleThemeChange(THEME_MODE.LIGHT);
},
},
{
id: 'system-theme',
name: 'Switch to System Theme',
keywords: 'system theme appearance',
section: 'Settings',
perform: (): void => {
handleThemeChange(THEME_MODE.SYSTEM);
},
},
];
return <KBarProvider actions={kbarActions}>{children}</KBarProvider>;
}

View File

@@ -565,8 +565,6 @@ export function QueryBuilderProvider({
setCurrentQuery((prevState) => { setCurrentQuery((prevState) => {
if (prevState.builder.queryData.length >= MAX_QUERIES) return prevState; if (prevState.builder.queryData.length >= MAX_QUERIES) return prevState;
console.log('prevState', prevState.builder.queryData);
const newQuery = createNewBuilderQuery(prevState.builder.queryData); const newQuery = createNewBuilderQuery(prevState.builder.queryData);
return { return {

View File

@@ -0,0 +1,50 @@
import React, {
createContext,
ReactNode,
useContext,
useMemo,
useState,
} from 'react';
type CmdKContextType = {
open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
openCmdK: () => void;
closeCmdK: () => void;
};
const CmdKContext = createContext<CmdKContextType | null>(null);
export function CmdKProvider({
children,
}: {
children: ReactNode;
}): JSX.Element {
const [open, setOpen] = useState<boolean>(false);
function openCmdK(): void {
setOpen(true);
}
function closeCmdK(): void {
setOpen(false);
}
const value = useMemo<CmdKContextType>(
() => ({
open,
setOpen,
openCmdK,
closeCmdK,
}),
[open],
);
return <CmdKContext.Provider value={value}>{children}</CmdKContext.Provider>;
}
export function useCmdK(): CmdKContextType {
const ctx = useContext(CmdKContext);
if (!ctx) throw new Error('useCmdK must be used inside CmdKProvider');
return ctx;
}

View File

@@ -0,0 +1,5 @@
export interface CreatePublicDashboardProps {
dashboardId: string;
timeRangeEnabled: boolean;
defaultTimeRange: string;
}

View File

@@ -0,0 +1,8 @@
export interface RevokePublicDashboardAccessProps {
id: string;
}
export interface PayloadProps {
status: string;
data: null;
}

View File

@@ -0,0 +1,16 @@
import { Dashboard } from "../getAll";
import { PublicDashboardMetaProps } from "./getMeta";
export interface PublicDashboardDataProps {
dashboard: Dashboard;
publicDashboard: PublicDashboardMetaProps;
}
export type GetPublicDashboardDataProps = {
id: string;
};
export interface PayloadProps {
data: PublicDashboardDataProps;
status: string;
}

View File

@@ -0,0 +1,14 @@
export interface PublicDashboardMetaProps {
timeRangeEnabled: boolean;
defaultTimeRange: string;
publicPath: string;
}
export type GetPublicDashboardMetaProps = {
id: string;
};
export interface PayloadProps {
data: PublicDashboardMetaProps;
status: string;
}

View File

@@ -0,0 +1,7 @@
export type GetPublicDashboardWidgetDataProps = {
id: string;
index: number;
startTime: number;
endTime: number;
};

View File

@@ -0,0 +1,6 @@
export interface UpdatePublicDashboardProps {
dashboardId: string;
timeRangeEnabled?: boolean;
defaultTimeRange?: string;
}

View File

@@ -126,4 +126,5 @@ export const routePermission: Record<keyof typeof ROUTES, ROLES[]> = {
METER_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'], METER_EXPLORER: ['ADMIN', 'EDITOR', 'VIEWER'],
METER: ['ADMIN', 'EDITOR', 'VIEWER'], METER: ['ADMIN', 'EDITOR', 'VIEWER'],
METER_EXPLORER_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'], METER_EXPLORER_VIEWS: ['ADMIN', 'EDITOR', 'VIEWER'],
PUBLIC_DASHBOARD: ['ADMIN', 'EDITOR', 'VIEWER'],
}; };

View File

@@ -3551,6 +3551,20 @@
dependencies: dependencies:
"@radix-ui/react-primitive" "2.1.3" "@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-checkbox@^1.2.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz#db45ca8a6d5c056a92f74edbb564acee05318b79"
integrity sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==
dependencies:
"@radix-ui/primitive" "1.1.3"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-presence" "1.1.5"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-use-controllable-state" "1.2.2"
"@radix-ui/react-use-previous" "1.1.1"
"@radix-ui/react-use-size" "1.1.1"
"@radix-ui/react-collection@1.0.3": "@radix-ui/react-collection@1.0.3":
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.3.tgz#9595a66e09026187524a36c6e7e9c7d286469159" resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.3.tgz#9595a66e09026187524a36c6e7e9c7d286469159"
@@ -3569,7 +3583,7 @@
dependencies: dependencies:
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs@1.1.2": "@radix-ui/react-compose-refs@1.1.2", "@radix-ui/react-compose-refs@^1.1.1":
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30" resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30"
integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==
@@ -3586,6 +3600,26 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36" resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36"
integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA== integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==
"@radix-ui/react-dialog@^1.1.11", "@radix-ui/react-dialog@^1.1.6":
version "1.1.15"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz#1de3d7a7e9a17a9874d29c07f5940a18a119b632"
integrity sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==
dependencies:
"@radix-ui/primitive" "1.1.3"
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-dismissable-layer" "1.1.11"
"@radix-ui/react-focus-guards" "1.1.3"
"@radix-ui/react-focus-scope" "1.1.7"
"@radix-ui/react-id" "1.1.1"
"@radix-ui/react-portal" "1.1.9"
"@radix-ui/react-presence" "1.1.5"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-slot" "1.2.3"
"@radix-ui/react-use-controllable-state" "1.2.2"
aria-hidden "^1.2.4"
react-remove-scroll "^2.6.3"
"@radix-ui/react-direction@1.0.1": "@radix-ui/react-direction@1.0.1":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b" resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b"
@@ -3643,7 +3677,7 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "1.0.1" "@radix-ui/react-use-layout-effect" "1.0.1"
"@radix-ui/react-id@1.1.1": "@radix-ui/react-id@1.1.1", "@radix-ui/react-id@^1.1.0":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7" resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7"
integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg== integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==
@@ -3712,7 +3746,7 @@
"@babel/runtime" "^7.13.10" "@babel/runtime" "^7.13.10"
"@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-portal@1.1.9", "@radix-ui/react-portal@^1.0.1": "@radix-ui/react-portal@1.1.9":
version "1.1.9" version "1.1.9"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz#14c3649fe48ec474ac51ed9f2b9f5da4d91c4472" resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.9.tgz#14c3649fe48ec474ac51ed9f2b9f5da4d91c4472"
integrity sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ== integrity sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==
@@ -3752,6 +3786,13 @@
dependencies: dependencies:
"@radix-ui/react-slot" "1.2.3" "@radix-ui/react-slot" "1.2.3"
"@radix-ui/react-primitive@^2.0.2":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz#2626ea309ebd63bf5767d3e7fc4081f81b993df0"
integrity sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==
dependencies:
"@radix-ui/react-slot" "1.2.4"
"@radix-ui/react-roving-focus@1.0.4": "@radix-ui/react-roving-focus@1.0.4":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974"
@@ -3783,6 +3824,13 @@
dependencies: dependencies:
"@radix-ui/react-compose-refs" "1.1.2" "@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-slot@1.2.4":
version "1.2.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.4.tgz#63c0ba05fdf90cc49076b94029c852d7bac1fb83"
integrity sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==
dependencies:
"@radix-ui/react-compose-refs" "1.1.2"
"@radix-ui/react-tabs@1.0.4": "@radix-ui/react-tabs@1.0.4":
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2" resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2"
@@ -3897,6 +3945,11 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e"
integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ== integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==
"@radix-ui/react-use-previous@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz#1a1ad5568973d24051ed0af687766f6c7cb9b5b5"
integrity sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==
"@radix-ui/react-use-rect@1.0.1": "@radix-ui/react-use-rect@1.0.1":
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2"
@@ -4029,11 +4082,6 @@
rc-resize-observer "^1.3.1" rc-resize-observer "^1.3.1"
rc-util "^5.38.0" rc-util "^5.38.0"
"@reach/observe-rect@^1.1.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@reach/observe-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2"
integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ==
"@react-dnd/asap@^5.0.1": "@react-dnd/asap@^5.0.1":
version "5.0.2" version "5.0.2"
resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488" resolved "https://registry.yarnpkg.com/@react-dnd/asap/-/asap-5.0.2.tgz#1f81f124c1cd6f39511c11a881cfb0f715343488"
@@ -4273,6 +4321,35 @@
tailwind-merge "^2.5.2" tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7" tailwindcss-animate "^1.0.7"
"@signozhq/checkbox@0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/checkbox/-/checkbox-0.0.2.tgz#d11fb5eff3927c540937e3bd24351bfc1fdef9ec"
integrity sha512-odQdh839GaTy1kqC8yavUKrOYP5tiIppUIV7xGNyxs/KnLGDWLw3ZSdACRV1Z55CLddjQ6OWKiwyVV7t+sxEuw==
dependencies:
"@radix-ui/react-checkbox" "^1.2.3"
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lucide-react "^0.445.0"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/command@0.0.0":
version "0.0.0"
resolved "https://registry.yarnpkg.com/@signozhq/command/-/command-0.0.0.tgz#bd1e1cac7346e862dd61df64b756302e89e1a322"
integrity sha512-AwRYxZTi4o8SBOL4hmgcgbhCKXl2Qb/TUSLbSYEMFdiQSl5VYA8XZJv5fSYVMJkAIlOaHzFzR04XNEU7lZcBpw==
dependencies:
"@radix-ui/react-dialog" "^1.1.11"
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
cmdk "^1.1.1"
lucide-react "^0.445.0"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/design-tokens@1.1.4": "@signozhq/design-tokens@1.1.4":
version "1.1.4" version "1.1.4"
resolved "https://registry.yarnpkg.com/@signozhq/design-tokens/-/design-tokens-1.1.4.tgz#5d5de5bd9d19b6a3631383db015cc4b70c3f7661" resolved "https://registry.yarnpkg.com/@signozhq/design-tokens/-/design-tokens-1.1.4.tgz#5d5de5bd9d19b6a3631383db015cc4b70c3f7661"
@@ -7364,6 +7441,16 @@ clsx@^2.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
cmdk@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/cmdk/-/cmdk-1.1.1.tgz#b8524272699ccaa37aaf07f36850b376bf3d58e5"
integrity sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==
dependencies:
"@radix-ui/react-compose-refs" "^1.1.1"
"@radix-ui/react-dialog" "^1.1.6"
"@radix-ui/react-id" "^1.1.0"
"@radix-ui/react-primitive" "^2.0.2"
co@^4.6.0: co@^4.6.0:
version "4.6.0" version "4.6.0"
resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz"
@@ -9406,11 +9493,6 @@ fast-diff@^1.1.2:
resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz" resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-equals@^2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/fast-equals/-/fast-equals-2.0.4.tgz#3add9410585e2d7364c2deeb6a707beadb24b927"
integrity sha512-caj/ZmjHljPrZtbzJ3kfH5ia/k4mTJe/qSiXAGzxZWRZgsgDV0cvNaQULqUX8t0/JVlzzEdYOwCN5DmzTxoD4w==
fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.3.0: fast-glob@^3.2.11, fast-glob@^3.2.7, fast-glob@^3.3.0:
version "3.3.3" version "3.3.3"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
@@ -9764,11 +9846,6 @@ functions-have-names@^1.2.2, functions-have-names@^1.2.3:
resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
fuse.js@^6.6.2:
version "6.6.2"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-6.6.2.tgz#fe463fed4b98c0226ac3da2856a415576dc9a111"
integrity sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==
gensync@^1.0.0-beta.2: gensync@^1.0.0-beta.2:
version "1.0.0-beta.2" version "1.0.0-beta.2"
resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"
@@ -11985,17 +12062,6 @@ kapsule@1, kapsule@^1.14:
dependencies: dependencies:
lodash-es "4" lodash-es "4"
kbar@0.1.0-beta.48:
version "0.1.0-beta.48"
resolved "https://registry.yarnpkg.com/kbar/-/kbar-0.1.0-beta.48.tgz#2db254cb2943f14200c5a5f47064135737527983"
integrity sha512-HD5A1dqfK6XGeoH4fRWTmRt4y76sDbtGxY4Dh2xNa5MYtvtKsqfz+nRZ0tKgcrjjGYN4rf5TLXMJuiE7Pb8rXg==
dependencies:
"@radix-ui/react-portal" "^1.0.1"
fast-equals "^2.0.3"
fuse.js "^6.6.2"
react-virtual "^2.8.2"
tiny-invariant "^1.2.0"
keyv@^4.0.0: keyv@^4.0.0:
version "4.5.4" version "4.5.4"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93"
@@ -15563,13 +15629,6 @@ react-use@^17.3.2:
ts-easing "^0.2.0" ts-easing "^0.2.0"
tslib "^2.1.0" tslib "^2.1.0"
react-virtual@^2.8.2:
version "2.10.4"
resolved "https://registry.yarnpkg.com/react-virtual/-/react-virtual-2.10.4.tgz#08712f0acd79d7d6f7c4726f05651a13b24d8704"
integrity sha512-Ir6+oPQZTVHfa6+JL9M7cvMILstFZH/H3jqeYeKI4MSUX+rIruVwFC6nGVXw9wqAw8L0Kg2KvfXxI85OvYQdpQ==
dependencies:
"@reach/observe-rect" "^1.1.0"
react-virtuoso@4.0.3: react-virtuoso@4.0.3:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.0.3.tgz#0dc8b10978095852d985b064157639b9fb9d9b1e" resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.0.3.tgz#0dc8b10978095852d985b064157639b9fb9d9b1e"
@@ -17284,11 +17343,6 @@ tiny-invariant@^1.0.2, tiny-invariant@^1.0.6:
resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz"
integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==
tiny-invariant@^1.2.0:
version "1.3.3"
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127"
integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==
tiny-warning@^1.0.0: tiny-warning@^1.0.0:
version "1.0.3" version "1.0.3"
resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz"

9
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/ClickHouse/clickhouse-go/v2 v2.40.1 github.com/ClickHouse/clickhouse-go/v2 v2.40.1
github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd github.com/SigNoz/govaluate v0.0.0-20240203125216-988004ccc7fd
github.com/SigNoz/signoz-otel-collector v0.129.4 github.com/SigNoz/signoz-otel-collector v0.129.10-rc.9
github.com/antlr4-go/antlr/v4 v4.13.1 github.com/antlr4-go/antlr/v4 v4.13.1
github.com/antonmedv/expr v1.15.3 github.com/antonmedv/expr v1.15.3
github.com/cespare/xxhash/v2 v2.3.0 github.com/cespare/xxhash/v2 v2.3.0
@@ -86,12 +86,19 @@ require (
) )
require ( require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 // indirect github.com/redis/go-redis/extra/rediscmd/v9 v9.15.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect
go.opentelemetry.io/collector/config/configretry v1.34.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
modernc.org/libc v1.66.10 // indirect modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect

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