Compare commits
25 Commits
v0.104.0-c
...
python_doc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
730986b719 | ||
|
|
497972f23c | ||
|
|
a9e30919d1 | ||
|
|
925c4c4a3d | ||
|
|
e66bfe5961 | ||
|
|
42943f72b7 | ||
|
|
7a72a209e5 | ||
|
|
44f00943a8 | ||
|
|
8867e1ef38 | ||
|
|
c08e520941 | ||
|
|
139cc4452d | ||
|
|
2f3baeb302 | ||
|
|
3d42b0058e | ||
|
|
ed70e3c5f5 | ||
|
|
7d6918f8b6 | ||
|
|
2885bc851e | ||
|
|
857258f8c3 | ||
|
|
ece5c2b7ad | ||
|
|
1078f98388 | ||
|
|
b4e2326f38 | ||
|
|
c79b154215 | ||
|
|
a59c0188cc | ||
|
|
3df426625a | ||
|
|
646f359f33 | ||
|
|
81167c6947 |
40
.github/CODEOWNERS
vendored
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
1
frontend/public/Logos/dashboards.svg
Normal 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 |
1
frontend/public/Logos/envoy.svg
Normal 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 |
1
frontend/public/Logos/fly-io.svg
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
1
frontend/public/Logos/honeycomb.svg
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
1
frontend/public/Logos/logs.svg
Normal 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 |
1
frontend/public/Logos/metrics.svg
Normal 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 |
1
frontend/public/Logos/traces.svg
Normal 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 |
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
28
frontend/src/api/dashboard/public/createPublicDashboard.ts
Normal 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;
|
||||||
20
frontend/src/api/dashboard/public/getPublicDashboardData.ts
Normal 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;
|
||||||
20
frontend/src/api/dashboard/public/getPublicDashboardMeta.ts
Normal 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;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
28
frontend/src/api/dashboard/public/updatePublicDashboard.ts
Normal 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;
|
||||||
2
frontend/src/auto-import-registry.d.ts
vendored
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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:');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
3
frontend/src/components/OverflowInputToolTip/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import OverflowInputToolTip from './OverflowInputToolTip';
|
||||||
|
|
||||||
|
export default OverflowInputToolTip;
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
55
frontend/src/components/cmdKPalette/cmdKPalette.scss
Normal 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;
|
||||||
|
}
|
||||||
336
frontend/src/components/cmdKPalette/cmdKPalette.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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="You’ve 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 ||
|
||||||
|
'You’ve 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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.error-details-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.error-container {
|
.error-container {
|
||||||
height: 50vh;
|
height: 50vh;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -209,6 +209,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.time-series-view-container {
|
||||||
|
.time-series-view-container-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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'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;
|
||||||
@@ -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" />;
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ function ExplorerColumnsRenderer({
|
|||||||
searchText={searchText}
|
searchText={searchText}
|
||||||
isAttributeKeySelected={isAttributeKeySelected}
|
isAttributeKeySelected={isAttributeKeySelected}
|
||||||
handleCheckboxChange={handleCheckboxChange}
|
handleCheckboxChange={handleCheckboxChange}
|
||||||
|
dataSource={initialDataSource}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
146
frontend/src/container/PublicDashboardContainer/Panel.tsx
Normal 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);
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import PublicDashboardContainer from './PublicDashboardContainer';
|
||||||
|
|
||||||
|
export default PublicDashboardContainer;
|
||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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: (
|
||||||
|
|||||||
18
frontend/src/hooks/dashboard/useGetPublicDashboardData.tsx
Normal 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,
|
||||||
|
});
|
||||||
19
frontend/src/hooks/dashboard/useGetPublicDashboardMeta.tsx
Normal 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,
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
328
frontend/src/mocks-server/__mockdata__/publicDashboard.ts
Normal 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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
80
frontend/src/pages/PublicDashboard/index.tsx
Normal 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;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.trace-explorer-time-series-view-container {
|
||||||
|
&-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
|
||||||
}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
50
frontend/src/providers/cmdKProvider.tsx
Normal 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;
|
||||||
|
}
|
||||||
5
frontend/src/types/api/dashboard/public/create.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface CreatePublicDashboardProps {
|
||||||
|
dashboardId: string;
|
||||||
|
timeRangeEnabled: boolean;
|
||||||
|
defaultTimeRange: string;
|
||||||
|
}
|
||||||
8
frontend/src/types/api/dashboard/public/delete.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export interface RevokePublicDashboardAccessProps {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayloadProps {
|
||||||
|
status: string;
|
||||||
|
data: null;
|
||||||
|
}
|
||||||
16
frontend/src/types/api/dashboard/public/get.ts
Normal 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;
|
||||||
|
}
|
||||||
14
frontend/src/types/api/dashboard/public/getMeta.ts
Normal 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;
|
||||||
|
}
|
||||||
7
frontend/src/types/api/dashboard/public/getWidgetData.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export type GetPublicDashboardWidgetDataProps = {
|
||||||
|
id: string;
|
||||||
|
index: number;
|
||||||
|
startTime: number;
|
||||||
|
endTime: number;
|
||||||
|
};
|
||||||
|
|
||||||
6
frontend/src/types/api/dashboard/public/update.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface UpdatePublicDashboardProps {
|
||||||
|
dashboardId: string;
|
||||||
|
timeRangeEnabled?: boolean;
|
||||||
|
defaultTimeRange?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||