mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-06 03:20:41 +01:00
Compare commits
20 Commits
fixes/dash
...
feat/googl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b506ec1627 | ||
|
|
7c1c298f6e | ||
|
|
160f0e0c70 | ||
|
|
1fbaccfe99 | ||
|
|
06f8a07638 | ||
|
|
80e0c2fe30 | ||
|
|
5367f39b61 | ||
|
|
a86b20925b | ||
|
|
8f89a1abf5 | ||
|
|
7ecf6f4b60 | ||
|
|
709b1a6745 | ||
|
|
b11a633331 | ||
|
|
e12d6f7a42 | ||
|
|
aefe6e76ab | ||
|
|
d27f94abce | ||
|
|
7774544809 | ||
|
|
2f48fc8ef8 | ||
|
|
946058210b | ||
|
|
113943771a | ||
|
|
3c517e5bde |
28
.github/CODEOWNERS
vendored
28
.github/CODEOWNERS
vendored
@@ -152,29 +152,39 @@ go.mod @therealpandey
|
||||
|
||||
## Dashboard Types
|
||||
|
||||
/frontend/src/types/api/dashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/types/api/widgets/ @SigNoz/pulse-frontend
|
||||
/frontend/src/api/types/dashboard/ @SigNoz/pulse-frontend
|
||||
|
||||
## Widget Card
|
||||
## Dashboard List
|
||||
|
||||
/frontend/src/container/WidgetCard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
||||
|
||||
# Dashboard Widget Page
|
||||
|
||||
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard Page
|
||||
|
||||
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
||||
|
||||
## Public Dashboard Page
|
||||
|
||||
/frontend/src/pages/PublicDashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/PublicDashboardContainer/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard Libs + Components
|
||||
|
||||
/frontend/src/lib/uPlotV2/ @SigNoz/pulse-frontend
|
||||
/frontend/src/lib/visualization/ @SigNoz/pulse-frontend
|
||||
/frontend/src/lib/dashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/lib/dashboardVariables/ @SigNoz/pulse-frontend
|
||||
/frontend/src/components/NewSelect/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard Pages
|
||||
|
||||
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
||||
## Dashboard V2
|
||||
/frontend/src/pages/DashboardPageV2/ @SigNoz/pulse-frontend
|
||||
/frontend/src/pages/DashboardsListPageV2/ @SigNoz/pulse-frontend
|
||||
|
||||
## Infrastructure Monitoring
|
||||
/frontend/src/pages/InfrastructureMonitoring/ @SigNoz/pulse-frontend
|
||||
|
||||
@@ -44,8 +44,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/subscription"
|
||||
"github.com/SigNoz/signoz/pkg/subscription/noopsubscription"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/dashboardtypes"
|
||||
@@ -89,9 +87,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(_ sqlstore.SQLStore, _ zeus.Zeus, _ organization.Getter, _ analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
|
||||
return nooplicensing.NewFactory()
|
||||
},
|
||||
func(_ zeus.Zeus, _ licensing.Licensing) subscription.Subscription {
|
||||
return noopsubscription.New()
|
||||
},
|
||||
signoz.NewEmailingProviderFactories(),
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(config.Global),
|
||||
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
eequerier "github.com/SigNoz/signoz/ee/querier"
|
||||
enterpriseapp "github.com/SigNoz/signoz/ee/query-service/app"
|
||||
eerules "github.com/SigNoz/signoz/ee/query-service/rules"
|
||||
"github.com/SigNoz/signoz/ee/subscription/httpsubscription"
|
||||
enterprisezeus "github.com/SigNoz/signoz/ee/zeus"
|
||||
"github.com/SigNoz/signoz/ee/zeus/httpzeus"
|
||||
"github.com/SigNoz/signoz/pkg/alertmanager"
|
||||
@@ -61,7 +60,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/ruler/signozruler"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
"github.com/SigNoz/signoz/pkg/sqlstore"
|
||||
"github.com/SigNoz/signoz/pkg/subscription"
|
||||
"github.com/SigNoz/signoz/pkg/telemetrystore"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/cloudintegrationtypes"
|
||||
@@ -105,9 +103,6 @@ func runServer(ctx context.Context, config signoz.Config, logger *slog.Logger) e
|
||||
func(sqlstore sqlstore.SQLStore, zeus zeus.Zeus, orgGetter organization.Getter, analytics analytics.Analytics) factory.ProviderFactory[licensing.Licensing, licensing.Config] {
|
||||
return httplicensing.NewProviderFactory(sqlstore, zeus, orgGetter, analytics)
|
||||
},
|
||||
func(zeus zeus.Zeus, licensing licensing.Licensing) subscription.Subscription {
|
||||
return httpsubscription.New(zeus, licensing)
|
||||
},
|
||||
signoz.NewEmailingProviderFactories(),
|
||||
signoz.NewCacheProviderFactories(),
|
||||
signoz.NewWebProviderFactories(config.Global),
|
||||
|
||||
@@ -9217,116 +9217,6 @@ components:
|
||||
required:
|
||||
- references
|
||||
type: object
|
||||
SubscriptiontypesGettableSubscription:
|
||||
properties:
|
||||
redirectURL:
|
||||
type: string
|
||||
required:
|
||||
- redirectURL
|
||||
type: object
|
||||
SubscriptiontypesGettableSubscriptionUsage:
|
||||
properties:
|
||||
billingPeriodEnd:
|
||||
format: int64
|
||||
type: integer
|
||||
billingPeriodStart:
|
||||
format: int64
|
||||
type: integer
|
||||
details:
|
||||
$ref: '#/components/schemas/SubscriptiontypesSubscriptionUsageDetails'
|
||||
discount:
|
||||
format: double
|
||||
type: number
|
||||
subscriptionStatus:
|
||||
type: string
|
||||
type: object
|
||||
SubscriptiontypesPostableSubscription:
|
||||
properties:
|
||||
url:
|
||||
type: string
|
||||
required:
|
||||
- url
|
||||
type: object
|
||||
SubscriptiontypesSubscriptionUsageBreakdown:
|
||||
properties:
|
||||
dayWiseBreakdown:
|
||||
$ref: '#/components/schemas/SubscriptiontypesSubscriptionUsageDayWiseBreakdown'
|
||||
tiers:
|
||||
items:
|
||||
$ref: '#/components/schemas/SubscriptiontypesSubscriptionUsageTier'
|
||||
nullable: true
|
||||
type: array
|
||||
type:
|
||||
type: string
|
||||
unit:
|
||||
type: string
|
||||
type: object
|
||||
SubscriptiontypesSubscriptionUsageDayWiseBreakdown:
|
||||
properties:
|
||||
breakdown:
|
||||
items:
|
||||
$ref: '#/components/schemas/SubscriptiontypesSubscriptionUsageDayWiseData'
|
||||
nullable: true
|
||||
type: array
|
||||
type:
|
||||
type: string
|
||||
type: object
|
||||
SubscriptiontypesSubscriptionUsageDayWiseData:
|
||||
properties:
|
||||
count:
|
||||
format: double
|
||||
type: number
|
||||
quantity:
|
||||
format: double
|
||||
type: number
|
||||
size:
|
||||
format: double
|
||||
type: number
|
||||
timestamp:
|
||||
format: int64
|
||||
type: integer
|
||||
total:
|
||||
format: double
|
||||
type: number
|
||||
unitPrice:
|
||||
format: double
|
||||
type: number
|
||||
type: object
|
||||
SubscriptiontypesSubscriptionUsageDetails:
|
||||
properties:
|
||||
baseFee:
|
||||
format: double
|
||||
type: number
|
||||
billTotal:
|
||||
format: double
|
||||
type: number
|
||||
breakdown:
|
||||
items:
|
||||
$ref: '#/components/schemas/SubscriptiontypesSubscriptionUsageBreakdown'
|
||||
nullable: true
|
||||
type: array
|
||||
total:
|
||||
format: double
|
||||
type: number
|
||||
type: object
|
||||
SubscriptiontypesSubscriptionUsageTier:
|
||||
properties:
|
||||
quantity:
|
||||
format: double
|
||||
type: number
|
||||
tierCost:
|
||||
format: double
|
||||
type: number
|
||||
tierEnd:
|
||||
format: int64
|
||||
type: integer
|
||||
tierStart:
|
||||
format: int64
|
||||
type: integer
|
||||
unitPrice:
|
||||
format: double
|
||||
type: number
|
||||
type: object
|
||||
TagtypesGettableTag:
|
||||
properties:
|
||||
key:
|
||||
@@ -14551,197 +14441,6 @@ paths:
|
||||
summary: Get stats
|
||||
tags:
|
||||
- stats
|
||||
/api/v1/subscriptions:
|
||||
get:
|
||||
deprecated: false
|
||||
description: This endpoint gets the organization's subscription along with its
|
||||
usage and billing details.
|
||||
operationId: GetSubscription
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SubscriptiontypesGettableSubscriptionUsage'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- subscription:read
|
||||
- tokenizer:
|
||||
- subscription:read
|
||||
summary: Get the subscription.
|
||||
tags:
|
||||
- subscriptions
|
||||
post:
|
||||
deprecated: false
|
||||
description: This endpoint creates a subscription for the organization.
|
||||
operationId: CreateSubscription
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SubscriptiontypesPostableSubscription'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SubscriptiontypesGettableSubscription'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- subscription:create
|
||||
- tokenizer:
|
||||
- subscription:create
|
||||
summary: Create a subscription.
|
||||
tags:
|
||||
- subscriptions
|
||||
put:
|
||||
deprecated: false
|
||||
description: This endpoint updates the organization's subscription.
|
||||
operationId: UpdateSubscription
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SubscriptiontypesPostableSubscription'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SubscriptiontypesGettableSubscription'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- subscription:list
|
||||
- subscription:update
|
||||
- tokenizer:
|
||||
- subscription:list
|
||||
- subscription:update
|
||||
summary: Update the subscription.
|
||||
tags:
|
||||
- subscriptions
|
||||
/api/v1/testChannel:
|
||||
post:
|
||||
deprecated: true
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
package httpsubscription
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/licensing"
|
||||
"github.com/SigNoz/signoz/pkg/subscription"
|
||||
"github.com/SigNoz/signoz/pkg/types/subscriptiontypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/SigNoz/signoz/pkg/zeus"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
const upstreamTimeout = 10 * time.Second
|
||||
|
||||
type provider struct {
|
||||
zeus zeus.Zeus
|
||||
licensing licensing.Licensing
|
||||
}
|
||||
|
||||
func New(zeus zeus.Zeus, licensing licensing.Licensing) subscription.Subscription {
|
||||
return &provider{
|
||||
zeus: zeus,
|
||||
licensing: licensing,
|
||||
}
|
||||
}
|
||||
|
||||
func (provider *provider) Create(ctx context.Context, organizationID valuer.UUID, postableSubscription *subscriptiontypes.PostableSubscription) (*subscriptiontypes.GettableSubscription, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, upstreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
license, err := provider.licensing.GetActive(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := json.Marshal(postableSubscription)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal subscription payload")
|
||||
}
|
||||
|
||||
response, err := provider.zeus.GetCheckoutURL(ctx, license.Key, body)
|
||||
if err != nil {
|
||||
if errors.Ast(err, errors.TypeAlreadyExists) {
|
||||
return nil, errors.WithAdditionalf(err, "checkout has already been completed for this account. Please click 'Refresh Status' to sync your subscription")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &subscriptiontypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Update(ctx context.Context, organizationID valuer.UUID, postableSubscription *subscriptiontypes.PostableSubscription) (*subscriptiontypes.GettableSubscription, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, upstreamTimeout)
|
||||
defer cancel()
|
||||
|
||||
license, err := provider.licensing.GetActive(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := json.Marshal(postableSubscription)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInvalidInput, errors.CodeInvalidInput, "failed to marshal subscription payload")
|
||||
}
|
||||
|
||||
response, err := provider.zeus.GetPortalURL(ctx, license.Key, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &subscriptiontypes.GettableSubscription{RedirectURL: gjson.GetBytes(response, "url").String()}, nil
|
||||
}
|
||||
|
||||
func (provider *provider) Get(ctx context.Context, organizationID valuer.UUID) (*subscriptiontypes.GettableSubscriptionUsage, error) {
|
||||
license, err := provider.licensing.GetActive(ctx, organizationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := provider.zeus.GetMeters(ctx, license.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
usage, err := subscriptiontypes.NewGettableSubscriptionUsage(data)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, errors.TypeInternal, zeus.ErrCodeResponseMalformed, "failed to unmarshal subscription usage")
|
||||
}
|
||||
|
||||
return usage, nil
|
||||
}
|
||||
@@ -323,10 +323,9 @@
|
||||
"name": "react",
|
||||
"importNames": [
|
||||
"createContext",
|
||||
"useContext",
|
||||
"useSyncExternalStore"
|
||||
"useContext"
|
||||
],
|
||||
"message": "[State mgmt] React Context and hand-rolled external stores are deprecated. Migrate shared state to Zustand."
|
||||
"message": "[State mgmt] React Context is deprecated. Migrate shared state to Zustand."
|
||||
},
|
||||
{
|
||||
"name": "immer",
|
||||
@@ -566,12 +565,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
// Root dashboard pages own the fetch lifecycle; useDashboardFetchRequired wraps it.
|
||||
// Root V2 pages own the dashboard fetch lifecycle; useDashboardFetchRequired wraps it.
|
||||
// Everywhere else must use useDashboardFetchRequired().
|
||||
"files": [
|
||||
"src/pages/DashboardPage/DashboardPage.tsx",
|
||||
"src/pages/DashboardPage/PanelEditorPage/PanelEditorPage.tsx",
|
||||
"src/pages/DashboardPage/DashboardContainer/hooks/useDashboardFetchRequired.ts"
|
||||
"src/pages/DashboardPageV2/DashboardPageV2.tsx",
|
||||
"src/pages/DashboardPageV2/PanelEditorPage/PanelEditorPage.tsx",
|
||||
"src/pages/DashboardPageV2/DashboardContainer/hooks/useDashboardFetchRequired.ts"
|
||||
],
|
||||
"rules": {
|
||||
"signoz/no-dashboard-fetch-outside-root": "off"
|
||||
|
||||
@@ -94,18 +94,23 @@ export const OnboardingV2 = Loadable(
|
||||
export const DashboardsListPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPage'
|
||||
/* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPageV2'
|
||||
),
|
||||
);
|
||||
|
||||
export const DashboardPage = Loadable(
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPage'),
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPageV2'),
|
||||
);
|
||||
|
||||
export const DashboardWidget = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "DashboardWidgetPage" */ 'pages/DashboardWidget'),
|
||||
);
|
||||
|
||||
export const DashboardPanelEditorPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "DashboardPanelEditorPage" */ 'pages/DashboardPage/PanelEditorPage/PanelEditorPage'
|
||||
/* webpackChunkName: "DashboardPanelEditorPage" */ 'pages/DashboardPageV2/PanelEditorPage/PanelEditorPage'
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
DashboardPage,
|
||||
DashboardPanelEditorPage,
|
||||
DashboardsListPage,
|
||||
DashboardWidget,
|
||||
EditRulesPage,
|
||||
ErrorDetails,
|
||||
ForgotPassword,
|
||||
@@ -182,6 +183,13 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: false,
|
||||
key: 'PUBLIC_DASHBOARD',
|
||||
},
|
||||
{
|
||||
path: ROUTES.DASHBOARD_WIDGET,
|
||||
exact: true,
|
||||
component: DashboardWidget,
|
||||
isPrivate: true,
|
||||
key: 'DASHBOARD_WIDGET',
|
||||
},
|
||||
{
|
||||
path: ROUTES.DASHBOARD_PANEL_EDITOR,
|
||||
exact: true,
|
||||
|
||||
27
frontend/src/api/dashboard/public/getPublicDashboardData.ts
Normal file
27
frontend/src/api/dashboard/public/getPublicDashboardData.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
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';
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useGetPublicDashboardData` hook (or `getPublicDashboardData` fetcher) from
|
||||
* `api/generated/services/dashboard` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
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;
|
||||
@@ -0,0 +1,34 @@
|
||||
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';
|
||||
|
||||
|
||||
/**
|
||||
* @deprecated Use the generated `useGetPublicDashboardWidgetQueryRange` hook (or `getPublicDashboardWidgetQueryRange` fetcher) from
|
||||
* `api/generated/services/dashboard` instead. This hand-written client targets the
|
||||
* same endpoint and will be removed once call sites migrate.
|
||||
*
|
||||
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
|
||||
*/
|
||||
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;
|
||||
@@ -10522,153 +10522,6 @@ export interface SpantypesUpdatableSpanMapperGroupDTO {
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesGettableSubscriptionDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
redirectURL: string;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesSubscriptionUsageDayWiseDataDTO {
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
count?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
quantity?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
size?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
timestamp?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
total?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
unitPrice?: number;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesSubscriptionUsageDayWiseBreakdownDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
breakdown?: SubscriptiontypesSubscriptionUsageDayWiseDataDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
type?: string;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesSubscriptionUsageTierDTO {
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
quantity?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
tierCost?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
tierEnd?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
tierStart?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
unitPrice?: number;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesSubscriptionUsageBreakdownDTO {
|
||||
dayWiseBreakdown?: SubscriptiontypesSubscriptionUsageDayWiseBreakdownDTO;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
tiers?: SubscriptiontypesSubscriptionUsageTierDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
unit?: string;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesSubscriptionUsageDetailsDTO {
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
baseFee?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
billTotal?: number;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
breakdown?: SubscriptiontypesSubscriptionUsageBreakdownDTO[] | null;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
total?: number;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesGettableSubscriptionUsageDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
billingPeriodEnd?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @format int64
|
||||
*/
|
||||
billingPeriodStart?: number;
|
||||
details?: SubscriptiontypesSubscriptionUsageDetailsDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
discount?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
subscriptionStatus?: string;
|
||||
}
|
||||
|
||||
export interface SubscriptiontypesPostableSubscriptionDTO {
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type TelemetrytypesGettableFieldKeysDTOKeysAnyOf = {
|
||||
[key: string]: TelemetrytypesTelemetryFieldKeyDTO[];
|
||||
};
|
||||
@@ -11887,30 +11740,6 @@ export type GetStats200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetSubscription200 = {
|
||||
data: SubscriptiontypesGettableSubscriptionUsageDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateSubscription201 = {
|
||||
data: SubscriptiontypesGettableSubscriptionDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type UpdateSubscription200 = {
|
||||
data: SubscriptiontypesGettableSubscriptionDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetTraceAggregationsPathParameters = {
|
||||
traceID: string;
|
||||
};
|
||||
|
||||
@@ -1,280 +0,0 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'pnpm generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
CreateSubscription201,
|
||||
GetSubscription200,
|
||||
RenderErrorResponseDTO,
|
||||
SubscriptiontypesPostableSubscriptionDTO,
|
||||
UpdateSubscription200,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* This endpoint gets the organization's subscription along with its usage and billing details.
|
||||
* @summary Get the subscription.
|
||||
*/
|
||||
export const getSubscription = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<GetSubscription200>({
|
||||
url: `/api/v1/subscriptions`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetSubscriptionQueryKey = () => {
|
||||
return [`/api/v1/subscriptions`] as const;
|
||||
};
|
||||
|
||||
export const getGetSubscriptionQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getSubscription>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSubscription>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getGetSubscriptionQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof getSubscription>>> = ({
|
||||
signal,
|
||||
}) => getSubscription(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSubscription>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetSubscriptionQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getSubscription>>
|
||||
>;
|
||||
export type GetSubscriptionQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get the subscription.
|
||||
*/
|
||||
|
||||
export function useGetSubscription<
|
||||
TData = Awaited<ReturnType<typeof getSubscription>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getSubscription>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetSubscriptionQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
return { ...query, queryKey: queryOptions.queryKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get the subscription.
|
||||
*/
|
||||
export const invalidateGetSubscription = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetSubscriptionQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* This endpoint creates a subscription for the organization.
|
||||
* @summary Create a subscription.
|
||||
*/
|
||||
export const createSubscription = (
|
||||
subscriptiontypesPostableSubscriptionDTO?: BodyType<SubscriptiontypesPostableSubscriptionDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateSubscription201>({
|
||||
url: `/api/v1/subscriptions`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: subscriptiontypesPostableSubscriptionDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateSubscriptionMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createSubscription'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof createSubscription>>,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createSubscription(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateSubscriptionMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createSubscription>>
|
||||
>;
|
||||
export type CreateSubscriptionMutationBody =
|
||||
| BodyType<SubscriptiontypesPostableSubscriptionDTO>
|
||||
| undefined;
|
||||
export type CreateSubscriptionMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create a subscription.
|
||||
*/
|
||||
export const useCreateSubscription = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getCreateSubscriptionMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* This endpoint updates the organization's subscription.
|
||||
* @summary Update the subscription.
|
||||
*/
|
||||
export const updateSubscription = (
|
||||
subscriptiontypesPostableSubscriptionDTO?: BodyType<SubscriptiontypesPostableSubscriptionDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<UpdateSubscription200>({
|
||||
url: `/api/v1/subscriptions`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: subscriptiontypesPostableSubscriptionDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateSubscriptionMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateSubscription'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof updateSubscription>>,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return updateSubscription(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateSubscriptionMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateSubscription>>
|
||||
>;
|
||||
export type UpdateSubscriptionMutationBody =
|
||||
| BodyType<SubscriptiontypesPostableSubscriptionDTO>
|
||||
| undefined;
|
||||
export type UpdateSubscriptionMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update the subscription.
|
||||
*/
|
||||
export const useUpdateSubscription = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateSubscription>>,
|
||||
TError,
|
||||
{ data?: BodyType<SubscriptiontypesPostableSubscriptionDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateSubscriptionMutationOptions(options));
|
||||
};
|
||||
20
frontend/src/api/v1/dashboards/id/get.ts
Normal file
20
frontend/src/api/v1/dashboards/id/get.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 { PayloadProps, Props } from 'types/api/dashboard/get';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
const get = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
|
||||
try {
|
||||
const response = await axios.get<PayloadProps>(`/dashboards/${props.id}`);
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
||||
23
frontend/src/api/v1/dashboards/id/update.ts
Normal file
23
frontend/src/api/v1/dashboards/id/update.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
import { PayloadProps, Props } from 'types/api/dashboard/update';
|
||||
|
||||
const update = async (props: Props): Promise<SuccessResponseV2<Dashboard>> => {
|
||||
try {
|
||||
const response = await axios.put<PayloadProps>(`/dashboards/${props.id}`, {
|
||||
...props.data,
|
||||
});
|
||||
|
||||
return {
|
||||
httpStatusCode: response.status,
|
||||
data: response.data.data,
|
||||
};
|
||||
} catch (error) {
|
||||
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
|
||||
}
|
||||
};
|
||||
|
||||
export default update;
|
||||
@@ -6,7 +6,6 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { DynamicVariableSuggestion } from 'providers/Dashboard/store/dynamicVariableSuggestions';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
@@ -546,22 +545,20 @@ function reduceQueriesToObject(queryArray: any[]): {
|
||||
/**
|
||||
* Prepares V5 query range payload from GetQueryResultsProps
|
||||
*/
|
||||
export const prepareQueryRangePayloadV5 = (
|
||||
{
|
||||
query,
|
||||
globalSelectedInterval,
|
||||
graphType,
|
||||
selectedTime,
|
||||
tableParams,
|
||||
variables = {},
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
formatForWeb,
|
||||
originalGraphType,
|
||||
fillGaps,
|
||||
}: GetQueryResultsProps,
|
||||
dynamicVariables: DynamicVariableSuggestion[] = [],
|
||||
): PrepareQueryRangePayloadV5Result => {
|
||||
export const prepareQueryRangePayloadV5 = ({
|
||||
query,
|
||||
globalSelectedInterval,
|
||||
graphType,
|
||||
selectedTime,
|
||||
tableParams,
|
||||
variables = {},
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
formatForWeb,
|
||||
originalGraphType,
|
||||
fillGaps,
|
||||
dynamicVariables,
|
||||
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
|
||||
let legendMap: Record<string, string> = {};
|
||||
const requestType = mapPanelTypeToRequestType(graphType);
|
||||
let queries: QueryEnvelope[] = [];
|
||||
@@ -674,9 +671,9 @@ export const prepareQueryRangePayloadV5 = (
|
||||
(acc, [key, value]) => {
|
||||
acc[key] = {
|
||||
value,
|
||||
type: dynamicVariables.some((v) => v.name === key)
|
||||
? ('dynamic' as VariableType)
|
||||
: undefined,
|
||||
type: dynamicVariables
|
||||
?.find((v) => v.name === key)
|
||||
?.type?.toLowerCase() as VariableType,
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
|
||||
@@ -5,16 +5,16 @@ import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card } from 'container/WidgetCard/styles';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
|
||||
|
||||
@@ -5,15 +5,15 @@ import { useHistory, useLocation } from 'react-router-dom';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card } from 'container/WidgetCard/styles';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { getStartAndEndTimesInMilliseconds } from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { CaptureDataProps } from '../CeleryTaskDetail/CeleryTaskDetail';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSelector } from 'react-redux';
|
||||
import { Card } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { CardContainer } from 'container/WidgetCard/styles';
|
||||
import { CardContainer } from 'container/GridCardLayout/styles';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { ChevronDown, ChevronUp } from '@signozhq/icons';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -6,9 +6,9 @@ import { Col, Row } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card } from 'container/WidgetCard/styles';
|
||||
import { ViewMenuAction } from 'container/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { Button } from 'container/MetricsApplication/Tabs/styles';
|
||||
import { useGraphClickHandler } from 'container/MetricsApplication/Tabs/util';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
@@ -8,7 +8,7 @@ import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { getQueryPayloadFromWidgetsData } from 'pages/Celery/CeleryOverview/CeleryOverviewUtils';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { History, Location } from 'history';
|
||||
import getRenderer from 'lib/uPlotLib/utils/getRenderer';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -4,9 +4,10 @@ import { useSelector } from 'react-redux';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import useUpdatedQuery from 'container/WidgetCard/hooks/useResolveQuery';
|
||||
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
@@ -79,6 +80,7 @@ export function useNavigateToExplorer(): (
|
||||
);
|
||||
|
||||
const { getUpdatedQuery } = useUpdatedQuery();
|
||||
const { dashboardData } = useDashboardStore();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
return useCallback(
|
||||
@@ -110,6 +112,7 @@ export function useNavigateToExplorer(): (
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
},
|
||||
dashboardData,
|
||||
})
|
||||
.then((query) => {
|
||||
preparedQuery = query;
|
||||
@@ -133,6 +136,13 @@ export function useNavigateToExplorer(): (
|
||||
|
||||
window.open(withBasePath(newExplorerPath), sameTab ? '_self' : '_blank');
|
||||
},
|
||||
[prepareQuery, minTime, maxTime, getUpdatedQuery, notifications],
|
||||
[
|
||||
prepareQuery,
|
||||
minTime,
|
||||
maxTime,
|
||||
getUpdatedQuery,
|
||||
dashboardData,
|
||||
notifications,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from 'chart.js';
|
||||
import annotationPlugin from 'chartjs-plugin-annotation';
|
||||
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
|
||||
import { generateGridTitle } from 'utils/generateGridTitle';
|
||||
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
|
||||
queryOperatorSuggestions,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
|
||||
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { debounce, isNull } from 'lodash-es';
|
||||
@@ -258,7 +258,10 @@ function QuerySearch({
|
||||
const lastValueRef = useRef<string>('');
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
const dashboardDynamicVariables = useDynamicVariableSuggestions();
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
'DYNAMIC',
|
||||
'values',
|
||||
);
|
||||
|
||||
// Add back the generateOptions function and useEffect
|
||||
const generateOptions = (keys: {
|
||||
@@ -1185,8 +1188,8 @@ function QuerySearch({
|
||||
);
|
||||
|
||||
// Add dynamic variables suggestions for the current key
|
||||
const variableName = dashboardDynamicVariables.find(
|
||||
(variable) => variable.attribute === keyName,
|
||||
const variableName = dashboardDynamicVariables?.find(
|
||||
(variable) => variable?.dynamicVariablesAttribute === keyName,
|
||||
)?.name;
|
||||
|
||||
if (variableName) {
|
||||
|
||||
@@ -17,6 +17,12 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { dashboardData: undefined } => ({
|
||||
dashboardData: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Shrink the suggestion-fetch debounce (300ms in prod) so these integration
|
||||
// tests aren't paced by it; coalescing semantics stay intact.
|
||||
jest.mock('../QuerySearch/constants', () => ({
|
||||
|
||||
@@ -21,6 +21,12 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { dashboardData: undefined } => ({
|
||||
dashboardData: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => {
|
||||
const handleRunQuery = jest.fn();
|
||||
return {
|
||||
@@ -146,16 +152,15 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// Wait for the mount fetch specifically. A debounced fetch from an earlier test
|
||||
// can still land after mockClear(), so waiting on "any call" would let this
|
||||
// assert against that one instead and make the result order-dependent.
|
||||
await waitFor(
|
||||
() =>
|
||||
expect(mockedGetKeysOnMount).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ signal: DataSource.LOGS, searchText: '' }),
|
||||
),
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
// Wait for debounced API call (300ms debounce + some buffer)
|
||||
await waitFor(() => expect(mockedGetKeysOnMount).toHaveBeenCalled(), {
|
||||
timeout: 2000,
|
||||
});
|
||||
|
||||
const lastArgs = mockedGetKeysOnMount.mock.calls[
|
||||
mockedGetKeysOnMount.mock.calls.length - 1
|
||||
]?.[0] as { signal: unknown; searchText: string };
|
||||
expect(lastArgs).toMatchObject({ signal: DataSource.LOGS, searchText: '' });
|
||||
});
|
||||
|
||||
it('calls provided onRun on Mod-Enter', async () => {
|
||||
|
||||
@@ -31,6 +31,12 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { dashboardData: undefined } => ({
|
||||
dashboardData: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||
getKeySuggestions: jest.fn().mockResolvedValue({
|
||||
data: { data: { keys: {} } },
|
||||
|
||||
@@ -525,34 +525,6 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Canonical name for a comparison's operator, limited to the equality and
|
||||
* membership forms. Every other shape (LIKE, BETWEEN, EXISTS, CONTAINS, REGEXP,
|
||||
* the ordering operators) returns undefined, so an operator-restricted removal
|
||||
* leaves it in place.
|
||||
*
|
||||
* The ANTLR4 runtime returns null for an absent token or rule despite the
|
||||
* non-nullable TypeScript signatures.
|
||||
*/
|
||||
const getComparisonOperator = (ctx: ComparisonContext): string | undefined => {
|
||||
if ((ctx.inClause() as unknown) !== null) {
|
||||
return 'in';
|
||||
}
|
||||
if ((ctx.notInClause() as unknown) !== null) {
|
||||
return 'not in';
|
||||
}
|
||||
if ((ctx.EQUALS() as unknown) !== null) {
|
||||
return '=';
|
||||
}
|
||||
if (
|
||||
(ctx.NOT_EQUALS() as unknown) !== null ||
|
||||
(ctx.NEQ() as unknown) !== null
|
||||
) {
|
||||
return '!=';
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes clauses for specified keys from a filter query expression.
|
||||
*
|
||||
@@ -570,16 +542,12 @@ const getComparisonOperator = (ctx: ComparisonContext): string | undefined => {
|
||||
* - `true`: removes only the first clause whose value contains any `$`.
|
||||
* - `string` (e.g. `"$service.name"`): removes only the clause whose value exactly
|
||||
* matches that string — preferred when the specific variable reference is known.
|
||||
* @param operatorsToRemove - When given, restricts removal to clauses whose operator
|
||||
* is in this set (`=`, `!=`, `in`, `not in`); every other clause on the key is kept.
|
||||
* Omit to remove a matching key's clauses whatever their operator.
|
||||
* @returns The rewritten expression, or an empty string if all clauses were removed.
|
||||
*/
|
||||
export const removeKeysFromExpression = (
|
||||
expression: string,
|
||||
keysToRemove: string[],
|
||||
removeOnlyVariableExpressions: string | boolean = false,
|
||||
operatorsToRemove?: string[],
|
||||
): string => {
|
||||
if (!keysToRemove || keysToRemove.length === 0) {
|
||||
return expression;
|
||||
@@ -589,9 +557,6 @@ export const removeKeysFromExpression = (
|
||||
}
|
||||
|
||||
const keysSet = new Set(keysToRemove.map((k) => k.trim().toLowerCase()));
|
||||
const operatorsSet = operatorsToRemove
|
||||
? new Set(operatorsToRemove.map((op) => op.trim().toLowerCase()))
|
||||
: null;
|
||||
// Tracks keys for which a variable expression has already been removed.
|
||||
// Having multiple $-value clauses for the same key is invalid; we remove at most one.
|
||||
const removedVariableKeys = new Set<string>();
|
||||
@@ -693,13 +658,6 @@ export const removeKeysFromExpression = (
|
||||
return src(ctx);
|
||||
}
|
||||
|
||||
if (operatorsSet) {
|
||||
const operator = getComparisonOperator(ctx);
|
||||
if (!operator || !operatorsSet.has(operator)) {
|
||||
return src(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
if (removeOnlyVariableExpressions) {
|
||||
// Scope the value check to value nodes only — not the full comparison text —
|
||||
// so a key that contains '$' does not trigger removal when the value is a
|
||||
|
||||
@@ -1,526 +0,0 @@
|
||||
import {
|
||||
convertFiltersToExpression,
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import {
|
||||
Query,
|
||||
TagFilter,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
applyCheckboxToggle,
|
||||
clearFilterFromQuery,
|
||||
deriveCheckboxState,
|
||||
getNotInOperator,
|
||||
} from './checkboxFilterQuery';
|
||||
import { CheckedState } from '../../types';
|
||||
import { SectionType } from './v2/itemRules';
|
||||
|
||||
const KEY = 'service.name';
|
||||
|
||||
/**
|
||||
* Mini test framework
|
||||
* -------------------
|
||||
* `filters.items` is the source of truth the checkbox algebra mutates.
|
||||
* `filter.expression` is the derived value the backend actually reads, and it is
|
||||
* authoritatively rebuilt from the items on every URL round trip
|
||||
* (`useGetCompositeQueryParam` -> `convertFiltersToExpressionWithExistingQuery`).
|
||||
* That rebuild is additive, so `applyCheckboxToggle` re-derives its own clauses
|
||||
* into the expression itself: otherwise the round trip resurrects a clause the
|
||||
* toggle removed, or appends a duplicate of one it replaced.
|
||||
*
|
||||
* So a case does not assert the intermediate expression the toggle emits. It
|
||||
* asserts the pair that has to stay consistent:
|
||||
* - `items` : exact structured clauses after the toggle
|
||||
* - `expression` : the expression AFTER the round trip, which is what ships
|
||||
*
|
||||
* `runToggle` runs the real reducer, then feeds its output through the real
|
||||
* converter to get the shipped expression.
|
||||
*/
|
||||
|
||||
type SimpleItem = {
|
||||
key: string;
|
||||
op: string;
|
||||
value: TagFilterItem['value'];
|
||||
};
|
||||
|
||||
function toTagItem(item: SimpleItem, idx: number): TagFilterItem {
|
||||
return {
|
||||
id: `id-${idx}`,
|
||||
key: { key: item.key, type: 'tag' } as TagFilterItem['key'],
|
||||
op: item.op,
|
||||
value: item.value,
|
||||
};
|
||||
}
|
||||
|
||||
// Serialises items into an expression (via the app's own converter) so a case's
|
||||
// starting state is self-consistent (items and expression agree), the way it
|
||||
// would be in the app after a prior round trip.
|
||||
const serializeItems = (items: SimpleItem[]): string =>
|
||||
convertFiltersToExpression({ items: items.map(toTagItem), op: 'AND' })
|
||||
.expression;
|
||||
|
||||
function buildQuery(items: SimpleItem[], expression: string): Query {
|
||||
return {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
filters: { items: items.map(toTagItem), op: 'AND' },
|
||||
filter: { expression },
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as Query;
|
||||
}
|
||||
|
||||
// Simulates the URL round trip: rebuild the shipped expression from the items,
|
||||
// reconciled against whatever expression the toggle left behind. Trimmed to
|
||||
// absorb a converter quirk that leaves a trailing space when it widens an
|
||||
// operator in place (e.g. `=` -> `IN`).
|
||||
function roundTripExpression(
|
||||
items: TagFilterItem[],
|
||||
emittedExpression: string,
|
||||
): string {
|
||||
const filters: TagFilter = { items, op: 'AND' };
|
||||
const { filter } = convertFiltersToExpressionWithExistingQuery(
|
||||
filters,
|
||||
emittedExpression,
|
||||
);
|
||||
return (filter?.expression ?? '').trim();
|
||||
}
|
||||
|
||||
interface ToggleAction {
|
||||
value: string;
|
||||
checked: boolean;
|
||||
isOnlyOrAllClicked?: boolean;
|
||||
previousState?: CheckedState;
|
||||
sectionType?: SectionType;
|
||||
source?: QuickFiltersSource;
|
||||
attributeValues?: string[];
|
||||
}
|
||||
|
||||
interface ToggleCase {
|
||||
name: string;
|
||||
initial?: { items?: SimpleItem[]; expression?: string };
|
||||
action: ToggleAction;
|
||||
expected: { items: SimpleItem[]; expression: string };
|
||||
}
|
||||
|
||||
function runToggle(c: ToggleCase): { items: SimpleItem[]; expression: string } {
|
||||
const initialItems = c.initial?.items ?? [];
|
||||
const initialExpression =
|
||||
c.initial?.expression ?? serializeItems(initialItems);
|
||||
|
||||
const result = applyCheckboxToggle({
|
||||
currentQuery: buildQuery(initialItems, initialExpression),
|
||||
activeQueryIndex: 0,
|
||||
filter: { attributeKey: { key: KEY, type: 'tag' } } as never,
|
||||
source: c.action.source ?? QuickFiltersSource.LOGS_EXPLORER,
|
||||
attributeValues: c.action.attributeValues ?? ['a', 'b', 'c'],
|
||||
value: c.action.value,
|
||||
checked: c.action.checked,
|
||||
isOnlyOrAllClicked: c.action.isOnlyOrAllClicked ?? false,
|
||||
previousState: c.action.previousState,
|
||||
sectionType: c.action.sectionType,
|
||||
});
|
||||
|
||||
const active = result.builder.queryData[0];
|
||||
const items = active?.filters?.items ?? [];
|
||||
return {
|
||||
items: items.map((item) => ({
|
||||
key: item.key?.key ?? '',
|
||||
op: item.op,
|
||||
value: item.value,
|
||||
})),
|
||||
expression: roundTripExpression(items, active?.filter?.expression ?? ''),
|
||||
};
|
||||
}
|
||||
|
||||
// Flat list. Every row asserts both the structured items and the shipped
|
||||
// (round-tripped) expression, which must stay in sync.
|
||||
const TOGGLE_CASES: ToggleCase[] = [
|
||||
{
|
||||
name: 'no clause, checked -> IN',
|
||||
action: { value: 'a', checked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: 'a' }],
|
||||
expression: `service.name in ['a']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'no clause, unchecked -> NOT IN',
|
||||
action: { value: 'a', checked: false },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: 'a' }],
|
||||
expression: `service.name not in ['a']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'no clause, unchecked on infra -> not in',
|
||||
action: {
|
||||
value: 'a',
|
||||
checked: false,
|
||||
source: QuickFiltersSource.INFRA_MONITORING,
|
||||
},
|
||||
// `nin` is what the source asks for, but re-deriving the expression
|
||||
// normalises it. Nothing observes the difference: both infra pages send
|
||||
// `filter.expression` and never `filters.items`.
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: 'a' }],
|
||||
expression: `service.name not in ['a']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'IN, check another value -> appended',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: ['a'] }] },
|
||||
action: { value: 'b', checked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: ['a', 'b'] }],
|
||||
expression: `service.name in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'IN, check when value is scalar -> promoted to array',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: 'a' }] },
|
||||
action: { value: 'b', checked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: ['a', 'b'] }],
|
||||
expression: `service.name in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'IN, uncheck one of many -> filtered out',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: ['a', 'b'] }] },
|
||||
action: { value: 'a', checked: false },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: ['b'] }],
|
||||
expression: `service.name in ['b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'IN, uncheck last value in array -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: ['a'] }] },
|
||||
action: { value: 'a', checked: false },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'IN, uncheck scalar value -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: 'a' }] },
|
||||
action: { value: 'a', checked: false },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'IN, uncheck in RELATED section -> replaced by NOT IN for that value',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: ['a', 'b'] }] },
|
||||
action: { value: 'a', checked: false, sectionType: SectionType.RELATED },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: 'a' }],
|
||||
expression: `service.name not in ['a']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, was unchecked then checked -> replaced by IN for that value',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: ['a'] }] },
|
||||
action: { value: 'b', checked: true, previousState: 'unchecked' },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: 'b' }],
|
||||
expression: `service.name in ['b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, re-checking an excluded value clears it, not flips it to IN',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: ['a'] }] },
|
||||
action: { value: 'a', checked: true, previousState: 'unchecked' },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, re-checking one of several excluded values keeps the rest',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }] },
|
||||
action: { value: 'a', checked: true, previousState: 'unchecked' },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['b'] }],
|
||||
expression: `service.name not in ['b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, exclude another value -> appended',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: ['a'] }] },
|
||||
action: { value: 'b', checked: false },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }],
|
||||
expression: `service.name not in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, exclude when scalar -> promoted to array',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: 'a' }] },
|
||||
action: { value: 'b', checked: false },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }],
|
||||
expression: `service.name not in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, check an excluded value -> removed from array',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }] },
|
||||
action: { value: 'a', checked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['b'] }],
|
||||
expression: `service.name not in ['b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, check last excluded value in array -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: ['a'] }] },
|
||||
action: { value: 'a', checked: true },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'NOT IN, check excluded scalar value -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: 'not in', value: 'a' }] },
|
||||
action: { value: 'a', checked: true },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: '= check another value -> promoted to IN array',
|
||||
initial: { items: [{ key: KEY, op: '=', value: 'a' }] },
|
||||
action: { value: 'b', checked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: ['a', 'b'] }],
|
||||
expression: `service.name in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '= uncheck -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: '=', value: 'a' }] },
|
||||
action: { value: 'a', checked: false },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: '!= exclude another value -> promoted to NOT IN array',
|
||||
initial: { items: [{ key: KEY, op: '!=', value: 'a' }] },
|
||||
action: { value: 'b', checked: false },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }],
|
||||
expression: `service.name not in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '!= exclude another value on infra -> not in array',
|
||||
initial: { items: [{ key: KEY, op: '!=', value: 'a' }] },
|
||||
action: {
|
||||
value: 'b',
|
||||
checked: false,
|
||||
source: QuickFiltersSource.INFRA_MONITORING,
|
||||
},
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'not in', value: ['a', 'b'] }],
|
||||
expression: `service.name not in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '!= check -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: '!=', value: 'a' }] },
|
||||
action: { value: 'a', checked: true },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'Only with no clause -> IN scalar',
|
||||
action: { value: 'a', checked: true, isOnlyOrAllClicked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: 'a' }],
|
||||
expression: `service.name in ['a']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Only replaces a multi-value IN with a single value',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: ['a', 'b'] }] },
|
||||
action: { value: 'a', checked: true, isOnlyOrAllClicked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: 'a' }],
|
||||
expression: `service.name in ['a']`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'All (clicking the sole selected value) -> clause gone',
|
||||
initial: { items: [{ key: KEY, op: 'in', value: ['a'] }] },
|
||||
action: { value: 'a', checked: true, isOnlyOrAllClicked: true },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'dropping the last clause keeps other keys in the expression',
|
||||
initial: {
|
||||
items: [{ key: KEY, op: 'in', value: 'a' }],
|
||||
expression: `${KEY} = 'a' AND http.method = 'GET'`,
|
||||
},
|
||||
action: { value: 'a', checked: false },
|
||||
// The seeded items omit the http.method clause the expression carries;
|
||||
// re-deriving reconciles it back, which is why items is not empty here.
|
||||
expected: {
|
||||
items: [{ key: 'http.method', op: '=', value: 'GET' }],
|
||||
expression: `http.method = 'GET'`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dropping the last clause strips the prefixed spelling too',
|
||||
initial: {
|
||||
items: [{ key: 'resource.service.name', op: 'in', value: 'a' }],
|
||||
expression: `resource.service.name = 'a'`,
|
||||
},
|
||||
action: { value: 'a', checked: false },
|
||||
expected: { items: [], expression: '' },
|
||||
},
|
||||
{
|
||||
name: 'removing the value must keep a free-form clause on the same key',
|
||||
initial: {
|
||||
items: [{ key: KEY, op: '=', value: 'a' }],
|
||||
expression: `${KEY} = 'a' AND ${KEY} CONTAINS 'keepme'`,
|
||||
},
|
||||
action: { value: 'a', checked: false },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'contains', value: 'keepme' }],
|
||||
expression: `service.name CONTAINS 'keepme'`,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'a second clause on the same key must not survive an add',
|
||||
initial: {
|
||||
items: [{ key: KEY, op: 'in', value: ['a'] }],
|
||||
expression: `${KEY} IN ['a'] AND ${KEY} != 'z'`,
|
||||
},
|
||||
action: { value: 'b', checked: true },
|
||||
expected: {
|
||||
items: [{ key: KEY, op: 'in', value: ['a', 'b'] }],
|
||||
expression: `service.name in ['a', 'b']`,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('applyCheckboxToggle (items + shipped expression stay in sync)', () => {
|
||||
it.each(TOGGLE_CASES)('$name', (c) => {
|
||||
const got = runToggle(c);
|
||||
expect(got.items).toStrictEqual(c.expected.items);
|
||||
expect(got.expression).toBe(c.expected.expression);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getNotInOperator', () => {
|
||||
it('returns short "nin" for infra monitoring', () => {
|
||||
expect(getNotInOperator(QuickFiltersSource.INFRA_MONITORING)).toBe('nin');
|
||||
});
|
||||
|
||||
it('returns long "not in" for other sources', () => {
|
||||
expect(getNotInOperator(QuickFiltersSource.LOGS_EXPLORER)).toBe('not in');
|
||||
expect(getNotInOperator(QuickFiltersSource.TRACES_EXPLORER)).toBe('not in');
|
||||
});
|
||||
});
|
||||
|
||||
describe('deriveCheckboxState', () => {
|
||||
const attributeValues = ['a', 'b', 'c'];
|
||||
|
||||
const state = (items: TagFilterItem[] | undefined): Record<string, boolean> =>
|
||||
deriveCheckboxState({ attributeValues, filterItems: items, filterKey: KEY });
|
||||
|
||||
it('no clause for key -> everything checked', () => {
|
||||
expect(state([])).toStrictEqual({ a: true, b: true, c: true });
|
||||
expect(state(undefined)).toStrictEqual({ a: true, b: true, c: true });
|
||||
});
|
||||
|
||||
it('unrelated clause only -> everything checked', () => {
|
||||
expect(
|
||||
state([toTagItem({ key: 'other', op: 'in', value: ['a'] }, 0)]),
|
||||
).toStrictEqual({ a: true, b: true, c: true });
|
||||
});
|
||||
|
||||
it('IN [list] -> only listed values checked', () => {
|
||||
expect(
|
||||
state([toTagItem({ key: KEY, op: 'in', value: ['a', 'c'] }, 0)]),
|
||||
).toStrictEqual({ a: true, b: false, c: true });
|
||||
});
|
||||
|
||||
it('= "value" -> only that value checked', () => {
|
||||
expect(
|
||||
state([toTagItem({ key: KEY, op: '=', value: 'b' }, 0)]),
|
||||
).toStrictEqual({ a: false, b: true, c: false });
|
||||
});
|
||||
|
||||
it('NOT IN [list] -> everything except excluded checked', () => {
|
||||
expect(
|
||||
state([toTagItem({ key: KEY, op: 'not in', value: ['a'] }, 0)]),
|
||||
).toStrictEqual({ a: false, b: true, c: true });
|
||||
});
|
||||
|
||||
it('!= "value" -> everything except that value checked', () => {
|
||||
expect(
|
||||
state([toTagItem({ key: KEY, op: '!=', value: 'b' }, 0)]),
|
||||
).toStrictEqual({ a: true, b: false, c: true });
|
||||
});
|
||||
|
||||
it('matches by base key across context prefixes', () => {
|
||||
expect(
|
||||
state([
|
||||
toTagItem({ key: 'resource.service.name', op: 'in', value: ['a'] }, 0),
|
||||
]),
|
||||
).toStrictEqual({ a: true, b: false, c: false });
|
||||
});
|
||||
|
||||
it('coerces boolean / number values to string keys', () => {
|
||||
expect(
|
||||
deriveCheckboxState({
|
||||
attributeValues: ['true', '42'],
|
||||
filterItems: [toTagItem({ key: KEY, op: '=', value: true }, 0)],
|
||||
filterKey: KEY,
|
||||
}),
|
||||
).toStrictEqual({ true: true, '42': false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearFilterFromQuery', () => {
|
||||
it('removes the key from items and expression at the active index only', () => {
|
||||
const query = {
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
filters: {
|
||||
items: [
|
||||
toTagItem({ key: KEY, op: 'in', value: ['a'] }, 0),
|
||||
toTagItem({ key: 'http.method', op: '=', value: 'GET' }, 1),
|
||||
],
|
||||
op: 'AND',
|
||||
},
|
||||
filter: { expression: `${KEY} = 'a' AND http.method = 'GET'` },
|
||||
},
|
||||
{
|
||||
filters: {
|
||||
items: [toTagItem({ key: KEY, op: 'in', value: ['a'] }, 2)],
|
||||
op: 'AND',
|
||||
},
|
||||
filter: { expression: `${KEY} = 'a'` },
|
||||
},
|
||||
],
|
||||
},
|
||||
} as unknown as Query;
|
||||
|
||||
const result = clearFilterFromQuery({
|
||||
currentQuery: query,
|
||||
filter: { attributeKey: { key: KEY, type: 'tag' } } as never,
|
||||
activeQueryIndex: 0,
|
||||
});
|
||||
|
||||
const active = result.builder.queryData[0];
|
||||
expect(active.filters?.items).toStrictEqual([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ key: 'http.method' }),
|
||||
}),
|
||||
]);
|
||||
expect(active.filter?.expression).toBe(`http.method = 'GET'`);
|
||||
|
||||
// Other queries keep both halves: stripping their expression while leaving
|
||||
// their items alone only churned a clause the round trip put straight back.
|
||||
const other = result.builder.queryData[1];
|
||||
expect(other.filters?.items).toHaveLength(1);
|
||||
expect(other.filter?.expression).toBe(`${KEY} = 'a'`);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,5 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import {
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
removeKeysFromExpression,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
@@ -13,33 +10,13 @@ import { cloneDeep, isArray } from 'lodash-es';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { getKeySpellings, isKeyMatch } from './utils';
|
||||
import { isKeyMatch } from './utils';
|
||||
import { CheckedState } from '../../types';
|
||||
import { SectionType } from './v2/itemRules';
|
||||
|
||||
export const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
|
||||
export const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in', 'nin'];
|
||||
|
||||
// The operators this algebra emits, and so the only ones it may rewrite out of an
|
||||
// expression. A hand-written clause on the same key (CONTAINS, EXISTS, a range) is
|
||||
// none of its business and has to survive a toggle.
|
||||
const MANAGED_OPERATORS = [OPERATORS['='], OPERATORS['!='], 'in', 'not in'];
|
||||
|
||||
/**
|
||||
* Drops this filter's own clauses for `key` from `expression`, leaving every other
|
||||
* key and any clause the checkbox does not manage untouched. Matches all context
|
||||
* prefixes, since `isKeyMatch` treats `service.name` and `resource.service.name` as
|
||||
* the same filter but expression rewrites match keys literally.
|
||||
*/
|
||||
function removeManagedClauses(expression: string, key: string): string {
|
||||
return removeKeysFromExpression(
|
||||
expression,
|
||||
getKeySpellings(key),
|
||||
false,
|
||||
MANAGED_OPERATORS,
|
||||
);
|
||||
}
|
||||
|
||||
// Sources that use backend APIs expecting short operator format (e.g., 'nin' instead of 'not in')
|
||||
const SOURCES_WITH_SHORT_OPERATORS = [QuickFiltersSource.INFRA_MONITORING];
|
||||
|
||||
@@ -125,8 +102,8 @@ export function deriveCheckboxState({
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new query with this filter's clauses for the attribute key removed from
|
||||
* the active query, both from the structured filter items and the raw expression.
|
||||
* Returns a new query with every clause for this attribute key removed, both
|
||||
* from the structured filter items and the raw filter expression.
|
||||
*/
|
||||
export function clearFilterFromQuery({
|
||||
currentQuery,
|
||||
@@ -141,28 +118,24 @@ export function clearFilterFromQuery({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeManagedClauses(
|
||||
item.filter?.expression ?? '',
|
||||
filter.attributeKey.key,
|
||||
),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filter.attributeKey.key),
|
||||
) || [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeKeysFromExpression(item.filter?.expression ?? '', [
|
||||
filter.attributeKey.key,
|
||||
]),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
idx === activeQueryIndex
|
||||
? item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filter.attributeKey.key),
|
||||
) || []
|
||||
: [...(item.filters?.items || [])],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -221,6 +194,12 @@ export function applyCheckboxToggle({
|
||||
(q) => !isKeyMatch(q.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(query.filter.expression, [
|
||||
filter.attributeKey.key,
|
||||
]);
|
||||
}
|
||||
|
||||
if (isOnlyOrAll === 'Only') {
|
||||
const newFilterItem: TagFilterItem = {
|
||||
id: uuid(),
|
||||
@@ -288,6 +267,12 @@ export function applyCheckboxToggle({
|
||||
}
|
||||
return item;
|
||||
});
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
} else if (isArray(currentFilter.value)) {
|
||||
// if we are removing some value when the running operator is IN we filter.
|
||||
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
|
||||
@@ -324,10 +309,9 @@ export function applyCheckboxToggle({
|
||||
? currentFilter.value.includes(value)
|
||||
: currentFilter.value === value;
|
||||
|
||||
// When clicking an unchecked value that is not itself excluded, the user
|
||||
// wants to SELECT it: replace the NOT IN filter with IN [value]. A value
|
||||
// that IS in the exclusion list falls through to the removal branch below.
|
||||
if (previousState === 'unchecked' && checked && !isValueInFilter) {
|
||||
// When clicking unchecked "Other" item, user wants to SELECT it
|
||||
// Replace NOT IN filter with IN [value]
|
||||
if (previousState === 'unchecked' && checked) {
|
||||
const newFilter: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getOperatorValue(OPERATORS.IN),
|
||||
@@ -340,6 +324,12 @@ export function applyCheckboxToggle({
|
||||
}
|
||||
return item;
|
||||
});
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
} else if (!checked || !isValueInFilter) {
|
||||
// Add to NOT IN when:
|
||||
// - checked=false (user explicitly unchecked to exclude)
|
||||
@@ -379,6 +369,12 @@ export function applyCheckboxToggle({
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
@@ -388,6 +384,16 @@ export function applyCheckboxToggle({
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value === value ? null : currentFilter.value,
|
||||
};
|
||||
if (newFilter.value === null && query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
@@ -450,18 +456,6 @@ export function applyCheckboxToggle({
|
||||
}
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const synced = convertFiltersToExpressionWithExistingQuery(
|
||||
query.filters ?? { items: [], op: 'AND' },
|
||||
removeManagedClauses(
|
||||
query.filter?.expression ?? '',
|
||||
filter.attributeKey.key,
|
||||
),
|
||||
);
|
||||
query.filter = synced.filter;
|
||||
query.filters = synced.filters;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
|
||||
@@ -39,16 +39,3 @@ export function isKeyMatch(
|
||||
): boolean {
|
||||
return getKeyWithoutPrefix(itemKey) === getKeyWithoutPrefix(filterKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Every spelling of a key that `isKeyMatch` treats as equal: the base name plus
|
||||
* each context-prefixed form. Expression rewrites match keys literally, so they
|
||||
* need the whole list where the items side only needs `isKeyMatch`.
|
||||
*/
|
||||
export function getKeySpellings(key: string | undefined): string[] {
|
||||
const base = getKeyWithoutPrefix(key);
|
||||
if (!base) {
|
||||
return [];
|
||||
}
|
||||
return [base, ...FIELD_CONTEXT_PREFIXES.map((prefix) => `${prefix}.${base}`)];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { timeItems } from 'constants/timePreference';
|
||||
import { timeItems } from 'container/NewWidget/RightContainer/timeItems';
|
||||
|
||||
export const menuItems = timeItems.map((item) => ({
|
||||
key: item.enum,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import TimeItems, {
|
||||
timePreferance,
|
||||
timePreferenceType,
|
||||
} from 'constants/timePreference';
|
||||
} from 'container/NewWidget/RightContainer/timeItems';
|
||||
|
||||
import { menuItems } from './config';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Tooltip } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { CircleAlert } from '@signozhq/icons';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
|
||||
import { getBackgroundColorAndThresholdCheck } from './utils';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { evaluateThresholdWithConvertedValue } from 'container/WidgetCard/Panels/TablePanel/utils';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
import { evaluateThresholdWithConvertedValue } from 'container/GridTableComponent/utils';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
|
||||
function doesValueSatisfyThreshold(
|
||||
rawValue: number,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Uplot from 'components/Uplot';
|
||||
import GridTableComponent from 'container/WidgetCard/Panels/TablePanel';
|
||||
import GridValueComponent from 'container/WidgetCard/Panels/ValuePanel';
|
||||
import GridTableComponent from 'container/GridTableComponent';
|
||||
import GridValueComponent from 'container/GridValueComponent';
|
||||
import LogsPanelComponent from 'container/LogsPanelTable/LogsPanelComponent';
|
||||
import TracesTableComponent from 'container/TracesTableComponent/TracesTableComponent';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -16,6 +16,7 @@ const ROUTES = {
|
||||
APPLICATION: '/services',
|
||||
ALL_DASHBOARD: '/dashboard',
|
||||
DASHBOARD: '/dashboard/:dashboardId',
|
||||
DASHBOARD_WIDGET: '/dashboard/:dashboardId/:widgetId',
|
||||
DASHBOARD_PANEL_EDITOR: '/dashboard/:dashboardId/panel/:panelId',
|
||||
EDIT_ALERTS: '/alerts/edit',
|
||||
LIST_ALL_ALERT: '/alerts',
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { MessageContext } from 'api/ai-assistant/chat';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { AlertListTabs } from 'pages/AlertList/types';
|
||||
import { NEW_PANEL_ID } from 'pages/DashboardPage/DashboardContainer/PanelEditor/newPanelRoute';
|
||||
import { NEW_PANEL_ID } from 'pages/DashboardPageV2/DashboardContainer/PanelEditor/newPanelRoute';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
getAllEndpointsWidgetData,
|
||||
getGroupByFiltersFromGroupByValues,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Card } from 'antd';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
function MetricOverTimeGraph({
|
||||
widget,
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
getStatusCodeBarChartWidgetData,
|
||||
statusCodeWidgetInfo,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import { handleGraphClick } from 'container/WidgetCard/Card/utils';
|
||||
import { useGraphClickToShowButton } from 'container/WidgetCard/hooks/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/WidgetCard/hooks/useNavigateToExplorerPages';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import { handleGraphClick } from 'container/GridCardLayout/GridCard/utils';
|
||||
import { useGraphClickToShowButton } from 'container/GridCardLayout/useGraphClickToShowButton';
|
||||
import useNavigateToExplorerPages from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
@@ -23,7 +23,7 @@ import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import ErrorState from './ErrorState';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExecStats } from 'api/v5/v5';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
|
||||
@@ -17,7 +17,7 @@ jest.mock('container/ApiMonitoring/utils', () => ({
|
||||
getGroupByFiltersFromGroupByValues: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('container/WidgetCard/Card', () => ({
|
||||
jest.mock('container/GridCardLayout/GridCard', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ customOnRowClick }) => (
|
||||
<div data-testid="grid-card-mock">
|
||||
|
||||
@@ -21,12 +21,15 @@ interface MockQueryResult {
|
||||
}
|
||||
|
||||
// Mocks
|
||||
jest.mock('lib/visualization/charts/BarChart/BarChart', () => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div data-testid="bar-chart-mock" />),
|
||||
}));
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/BarChart/BarChart',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div data-testid="bar-chart-mock" />),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('components/CeleryTask/useGetGraphCustomSeries', () => ({
|
||||
useGetGraphCustomSeries: (): { getCustomSeries: jest.Mock } => ({
|
||||
@@ -40,7 +43,7 @@ jest.mock('components/CeleryTask/useNavigateToExplorer', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/WidgetCard/hooks/useGraphClickToShowButton', () => ({
|
||||
jest.mock('container/GridCardLayout/useGraphClickToShowButton', () => ({
|
||||
useGraphClickToShowButton: (): {
|
||||
componentClick: boolean;
|
||||
htmlRef: HTMLElement | null;
|
||||
@@ -50,7 +53,7 @@ jest.mock('container/WidgetCard/hooks/useGraphClickToShowButton', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/WidgetCard/hooks/useNavigateToExplorerPages', () => ({
|
||||
jest.mock('container/GridCardLayout/useNavigateToExplorerPages', () => ({
|
||||
__esModule: true,
|
||||
default: (): { navigateToExplorerPages: jest.Mock } => ({
|
||||
navigateToExplorerPages: jest.fn(),
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from 'components/QuickFilters/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { GraphClickMetaData } from 'container/WidgetCard/hooks/useNavigateToExplorerPages';
|
||||
import { GraphClickMetaData } from 'container/GridCardLayout/useNavigateToExplorerPages';
|
||||
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
|
||||
import { convertNanoToMilliseconds } from 'container/MetricsExplorer/Summary/utils';
|
||||
import dayjs from 'dayjs';
|
||||
@@ -18,7 +18,7 @@ import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { ArrowUpDown, ChevronDown, ChevronRight, Info } from '@signozhq/icons';
|
||||
import { getWidgetQuery } from 'pages/MessagingQueues/MQDetails/MetricPage/MetricPageUtil';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { Card, Flex } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import type { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { DrawStyle } from 'lib/uPlotV2/config/types';
|
||||
import type { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import type { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
@@ -98,7 +98,7 @@ jest.mock('api/channels/getAll', () => ({
|
||||
}));
|
||||
|
||||
// Mock alert format categories
|
||||
jest.mock('constants/formats/alertFormatCategories', () => ({
|
||||
jest.mock('container/NewWidget/RightContainer/alertFomatCategories', () => ({
|
||||
getCategoryByOptionId: jest.fn(() => ({ name: 'bytes' })),
|
||||
getCategorySelectOptionByName: jest.fn(() => [
|
||||
{ label: 'Bytes', value: 'bytes' },
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { useCreateAlertState } from 'container/CreateAlertV2/context';
|
||||
import ChartPreviewComponent from 'container/FormAlertRules/ChartPreview';
|
||||
import PlotTag from 'components/PlotTag/PlotTag';
|
||||
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
|
||||
import { AppState } from 'store/reducers';
|
||||
|
||||
@@ -44,7 +44,7 @@ jest.mock(
|
||||
},
|
||||
);
|
||||
jest.mock(
|
||||
'components/PlotTag/PlotTag',
|
||||
'container/NewWidget/LeftContainer/WidgetGraph/PlotTag',
|
||||
() =>
|
||||
function MockPlotTag(props: any): JSX.Element {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import ChartWrapper from 'lib/visualization/charts/ChartWrapper/ChartWrapper';
|
||||
import ChartWrapper from 'container/DashboardContainer/visualization/charts/ChartWrapper/ChartWrapper';
|
||||
import BarChartTooltip from 'lib/uPlotV2/components/Tooltip/BarChartTooltip';
|
||||
import {
|
||||
BarTooltipProps,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
|
||||
import { StackMode } from 'lib/uPlotV2/config/types';
|
||||
|
||||
import { BarChartProps } from 'lib/visualization/charts/types';
|
||||
import { BarChartProps } from '../types';
|
||||
|
||||
export default function BarChart(props: BarChartProps): JSX.Element {
|
||||
const {
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import ChartLayout from 'lib/visualization/layout/ChartLayout/ChartLayout';
|
||||
import ChartLayout from 'container/DashboardContainer/visualization/layout/ChartLayout/ChartLayout';
|
||||
import UPlotLegend from 'lib/uPlotV2/components/Legend/UPlotLegend';
|
||||
import {
|
||||
LegendPosition,
|
||||
@@ -13,8 +13,8 @@ import TooltipPlugin from 'lib/uPlotV2/plugins/TooltipPlugin/TooltipPlugin';
|
||||
import noop from 'lodash-es/noop';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { ChartWrapperProps } from 'lib/visualization/charts/types';
|
||||
import { useChartStacking } from 'lib/visualization/charts/ChartWrapper/useChartStacking';
|
||||
import { ChartWrapperProps } from '../types';
|
||||
import { useChartStacking } from './useChartStacking';
|
||||
|
||||
const TOOLTIP_WIDTH_PADDING = 120;
|
||||
const TOOLTIP_MIN_WIDTH = 300;
|
||||
@@ -3,7 +3,7 @@ import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { StackMode } from 'lib/uPlotV2/config/types';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { useChartStacking } from 'lib/visualization/charts/ChartWrapper/useChartStacking';
|
||||
import { useChartStacking } from '../useChartStacking';
|
||||
|
||||
type Hooks = Record<string, (...args: unknown[]) => void>;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { StackMode } from 'lib/uPlotV2/config/types';
|
||||
import { has } from 'lodash-es';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { stackSeries } from 'lib/visualization/charts/utils/stackSeriesUtils';
|
||||
import { stackSeries } from '../utils/stackSeriesUtils';
|
||||
|
||||
/** Returns true if the series at the given index is hidden (e.g. via legend toggle). */
|
||||
function isSeriesHidden(plot: uPlot, seriesIndex: number): boolean {
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import ChartWrapper from 'lib/visualization/charts/ChartWrapper/ChartWrapper';
|
||||
import ChartWrapper from 'container/DashboardContainer/visualization/charts/ChartWrapper/ChartWrapper';
|
||||
import HistogramTooltip from 'lib/uPlotV2/components/Tooltip/HistogramTooltip';
|
||||
import {
|
||||
HistogramTooltipProps,
|
||||
TooltipRenderArgs,
|
||||
} from 'lib/uPlotV2/components/types';
|
||||
|
||||
import { HistogramChartProps } from 'lib/visualization/charts/types';
|
||||
import { HistogramChartProps } from '../types';
|
||||
|
||||
export default function Histogram(props: HistogramChartProps): JSX.Element {
|
||||
const {
|
||||
@@ -9,18 +9,15 @@ import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import Legend from 'lib/uPlotV2/components/Legend/Legend';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
|
||||
import { PieChartProps, PieSlice } from 'lib/visualization/charts/types';
|
||||
import { calculateChartDimensions } from 'lib/visualization/charts/utils';
|
||||
import { PieChartProps, PieSlice } from '../types';
|
||||
import { calculateChartDimensions } from '../utils';
|
||||
|
||||
import { usePieInteractions } from 'lib/visualization/hooks/usePieInteractions';
|
||||
import PieArc from 'lib/visualization/charts/Pie/PieArc';
|
||||
import PieCenterLabel from 'lib/visualization/charts/Pie/PieCenterLabel';
|
||||
import styles from 'lib/visualization/charts/Pie/Pie.module.scss';
|
||||
import { PieTooltipData } from 'lib/visualization/charts/Pie/types';
|
||||
import {
|
||||
getDonutGeometry,
|
||||
getFillColor,
|
||||
} from 'lib/visualization/charts/Pie/utils';
|
||||
import { usePieInteractions } from '../../hooks/usePieInteractions';
|
||||
import PieArc from './PieArc';
|
||||
import PieCenterLabel from './PieCenterLabel';
|
||||
import styles from './Pie.module.scss';
|
||||
import { PieTooltipData } from './types';
|
||||
import { getDonutGeometry, getFillColor } from './utils';
|
||||
|
||||
/**
|
||||
* Donut chart rendered with @visx. Splits its area into chart + legend with the
|
||||
@@ -2,9 +2,9 @@ import type { MouseEvent as ReactMouseEvent } from 'react';
|
||||
import type { PrecisionOption } from 'components/Graph/types';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
|
||||
import { PieSlice } from 'lib/visualization/charts/types';
|
||||
import { PieSlice } from '../types';
|
||||
|
||||
import { getArcGeometry } from 'lib/visualization/charts/Pie/utils';
|
||||
import { getArcGeometry } from './utils';
|
||||
|
||||
// Slices below this share of the total don't get a leader label (too cramped).
|
||||
const MIN_LABEL_SHARE = 0.03;
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PrecisionOption } from 'components/Graph/types';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
|
||||
import { getScaledFontSize } from 'lib/visualization/charts/Pie/utils';
|
||||
import { getScaledFontSize } from './utils';
|
||||
|
||||
interface PieCenterLabelProps {
|
||||
/** Sum of the visible slice values, shown in the donut hole. */
|
||||
@@ -4,8 +4,8 @@ import { TooltipProvider } from '@signozhq/ui/tooltip';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { LegendItem } from 'lib/uPlotV2/config/types';
|
||||
|
||||
import { PieSlice } from 'lib/visualization/charts/types';
|
||||
import Pie from 'lib/visualization/charts/Pie/Pie';
|
||||
import { PieSlice } from '../../types';
|
||||
import Pie from '../Pie';
|
||||
|
||||
jest.mock('hooks/useDimensions', () => ({
|
||||
useResizeObserver: jest.fn().mockReturnValue({ width: 400, height: 300 }),
|
||||
@@ -1,7 +1,7 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { PieSlice } from 'lib/visualization/charts/types';
|
||||
import PieArc from 'lib/visualization/charts/Pie/PieArc';
|
||||
import { PieSlice } from '../../types';
|
||||
import PieArc from '../PieArc';
|
||||
|
||||
jest.mock('components/Graph/yAxisConfig', () => ({
|
||||
// Echo the raw value so assertions are deterministic.
|
||||
@@ -1,7 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
|
||||
|
||||
import PieCenterLabel from 'lib/visualization/charts/Pie/PieCenterLabel';
|
||||
import PieCenterLabel from '../PieCenterLabel';
|
||||
|
||||
jest.mock('components/Graph/yAxisConfig', () => ({
|
||||
getYAxisFormattedValue: jest.fn(),
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
getFillColor,
|
||||
getScaledFontSize,
|
||||
lightenColor,
|
||||
} from 'lib/visualization/charts/Pie/utils';
|
||||
} from '../utils';
|
||||
|
||||
describe('Pie utils', () => {
|
||||
describe('getDonutGeometry', () => {
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
DonutGeometry,
|
||||
ParsedRgb,
|
||||
ScaledFontSizeArgs,
|
||||
} from 'lib/visualization/charts/Pie/types';
|
||||
} from './types';
|
||||
|
||||
// Leader-line + two-line label/value drawn outside the donut. `getArcGeometry`
|
||||
// anchors the label at `radius * LABEL_RADIUS_RATIO`; `LABEL_TEXT_ALLOWANCE` is
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import ChartWrapper from 'lib/visualization/charts/ChartWrapper/ChartWrapper';
|
||||
import ChartWrapper from 'container/DashboardContainer/visualization/charts/ChartWrapper/ChartWrapper';
|
||||
import TimeSeriesTooltip from 'lib/uPlotV2/components/Tooltip/TimeSeriesTooltip';
|
||||
import {
|
||||
TimeSeriesTooltipProps,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
|
||||
import { StackMode } from 'lib/uPlotV2/config/types';
|
||||
|
||||
import { TimeSeriesChartProps } from 'lib/visualization/charts/types';
|
||||
import { TimeSeriesChartProps } from '../types';
|
||||
|
||||
export default function TimeSeries(props: TimeSeriesChartProps): JSX.Element {
|
||||
const { children, customTooltip, stack = StackMode.None, ...rest } = props;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
|
||||
import { calculateChartDimensions } from 'lib/visualization/charts/utils';
|
||||
import { calculateChartDimensions } from '../utils';
|
||||
|
||||
const labels = (count: number, length = 20): string[] =>
|
||||
Array.from({ length: count }, (_, i) =>
|
||||
@@ -1,4 +1,3 @@
|
||||
import uPlot from 'uplot';
|
||||
import type { MouseEvent as ReactMouseEvent } from 'react';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PrecisionOption } from 'components/Graph/types';
|
||||
@@ -110,13 +109,3 @@ export interface PieChartProps {
|
||||
onSliceClick?: (slice: PieSlice, event: ReactMouseEvent) => void;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
/** A uPlot series enriched with the aggregates the legend table shows. */
|
||||
export type ExtendedChartDataset = uPlot.Series & {
|
||||
show: boolean;
|
||||
sum: number;
|
||||
avg: number;
|
||||
min: number;
|
||||
max: number;
|
||||
index: number;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sortByMeanDesc } from 'lib/visualization/charts/utils/sortByMeanDesc';
|
||||
import { sortByMeanDesc } from '../sortByMeanDesc';
|
||||
|
||||
interface Item {
|
||||
name: string;
|
||||
@@ -2,7 +2,7 @@ import { AlignedData } from 'uplot';
|
||||
|
||||
import { StackMode } from 'lib/uPlotV2/config/types';
|
||||
|
||||
import { stackSeries } from 'lib/visualization/charts/utils/stackSeriesUtils';
|
||||
import { stackSeries } from '../stackSeriesUtils';
|
||||
|
||||
const includeAll = (): boolean => false;
|
||||
|
||||
@@ -6,15 +6,16 @@ import { ResizeTable } from 'components/ResizeTable';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { usePlotContext } from 'lib/uPlotV2/context/PlotContext';
|
||||
import useLegendsSync from 'lib/uPlotV2/hooks/useLegendsSync';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
|
||||
import { getChartManagerColumns } from 'lib/visualization/components/ChartManager/getChartMangerColumns';
|
||||
import {
|
||||
ExtendedChartDataset,
|
||||
getDefaultTableDataSet,
|
||||
} from 'lib/visualization/components/ChartManager/utils';
|
||||
import { getChartManagerColumns } from './getChartMangerColumns';
|
||||
import { ExtendedChartDataset, getDefaultTableDataSet } from './utils';
|
||||
|
||||
import 'lib/visualization/components/ChartManager/ChartManager.styles.scss';
|
||||
import './ChartManager.styles.scss';
|
||||
|
||||
interface ChartManagerProps {
|
||||
config: UPlotConfigBuilder;
|
||||
@@ -52,6 +53,7 @@ export default function ChartManager({
|
||||
onToggleSeriesVisibility,
|
||||
syncSeriesVisibilityToLocalStorage,
|
||||
} = usePlotContext();
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
|
||||
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(() =>
|
||||
getDefaultTableDataSet(
|
||||
@@ -117,6 +119,7 @@ export default function ChartManager({
|
||||
onToggleSeriesOnOff: handleToggleSeriesOnOff,
|
||||
onToggleSeriesVisibility,
|
||||
yAxisUnit,
|
||||
isGraphDisabled: isDashboardLocked,
|
||||
decimalPrecision,
|
||||
}),
|
||||
[
|
||||
@@ -125,6 +128,7 @@ export default function ChartManager({
|
||||
handleToggleSeriesOnOff,
|
||||
onToggleSeriesVisibility,
|
||||
yAxisUnit,
|
||||
isDashboardLocked,
|
||||
decimalPrecision,
|
||||
],
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Tooltip } from 'antd';
|
||||
|
||||
import 'lib/visualization/components/ChartManager/ChartManager.styles.scss';
|
||||
import './ChartManager.styles.scss';
|
||||
|
||||
interface SeriesLabelProps {
|
||||
label: string;
|
||||
@@ -2,7 +2,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
import ChartManager from 'lib/visualization/components/ChartManager/ChartManager';
|
||||
import ChartManager from '../ChartManager';
|
||||
|
||||
const mockSyncSeriesVisibilityToLocalStorage = jest.fn();
|
||||
const mockToastSuccess = jest.fn();
|
||||
@@ -32,6 +32,20 @@ jest.mock('lib/uPlotV2/hooks/useLegendsSync', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (
|
||||
selector?: (s: { dashboardData: { locked: boolean } | undefined }) => {
|
||||
dashboardData: { locked: boolean };
|
||||
},
|
||||
): { dashboardData: { locked: boolean } } => {
|
||||
const mockState = { dashboardData: { locked: false } };
|
||||
return selector ? selector(mockState) : mockState;
|
||||
},
|
||||
selectIsDashboardLocked: (s: {
|
||||
dashboardData: { locked: boolean } | undefined;
|
||||
}): boolean => s.dashboardData?.locked ?? false,
|
||||
}));
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: {
|
||||
@@ -1,7 +1,7 @@
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
|
||||
import { SeriesLabel } from 'lib/visualization/components/ChartManager/SeriesLabel';
|
||||
import { SeriesLabel } from '../SeriesLabel';
|
||||
|
||||
describe('SeriesLabel', () => {
|
||||
it('renders the label text', () => {
|
||||
@@ -2,8 +2,8 @@ import { render } from '@testing-library/react';
|
||||
import { Y_AXIS_UNIT_NAMES } from 'components/YAxisUnitSelector/constants';
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
|
||||
import { getChartManagerColumns } from 'lib/visualization/components/ChartManager/getChartMangerColumns';
|
||||
import { ExtendedChartDataset } from 'lib/visualization/components/ChartManager/utils';
|
||||
import { getChartManagerColumns } from '../getChartMangerColumns';
|
||||
import { ExtendedChartDataset } from '../utils';
|
||||
|
||||
const createMockDataset = (
|
||||
index: number,
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
formatTableValueWithUnit,
|
||||
getDefaultTableDataSet,
|
||||
getTableColumnTitle,
|
||||
} from 'lib/visualization/components/ChartManager/utils';
|
||||
} from '../utils';
|
||||
|
||||
describe('ChartManager utils', () => {
|
||||
describe('getDefaultTableDataSet', () => {
|
||||
@@ -1,13 +1,13 @@
|
||||
import { TableColumnType as ColumnType } from 'antd';
|
||||
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
|
||||
import CustomCheckBox from 'lib/visualization/components/ChartManager/CustomCheckBox';
|
||||
import CustomCheckBox from 'container/GridCardLayout/GridCard/FullView/TableRender/CustomCheckBox';
|
||||
|
||||
import { SeriesLabel } from 'lib/visualization/components/ChartManager/SeriesLabel';
|
||||
import { SeriesLabel } from './SeriesLabel';
|
||||
import {
|
||||
ExtendedChartDataset,
|
||||
formatTableValueWithUnit,
|
||||
getTableColumnTitle,
|
||||
} from 'lib/visualization/components/ChartManager/utils';
|
||||
} from './utils';
|
||||
|
||||
export interface GetChartManagerColumnsParams {
|
||||
tableDataSet: ExtendedChartDataset[];
|
||||
@@ -16,6 +16,7 @@ export interface GetChartManagerColumnsParams {
|
||||
onToggleSeriesVisibility: (index: number) => void;
|
||||
yAxisUnit?: string;
|
||||
decimalPrecision?: PrecisionOption;
|
||||
isGraphDisabled?: boolean;
|
||||
}
|
||||
|
||||
export function getChartManagerColumns({
|
||||
@@ -25,6 +26,7 @@ export function getChartManagerColumns({
|
||||
onToggleSeriesVisibility,
|
||||
yAxisUnit,
|
||||
decimalPrecision = PrecisionOptionsEnum.TWO,
|
||||
isGraphDisabled,
|
||||
}: GetChartManagerColumnsParams): ColumnType<ExtendedChartDataset>[] {
|
||||
return [
|
||||
{
|
||||
@@ -37,6 +39,7 @@ export function getChartManagerColumns({
|
||||
data={tableDataSet}
|
||||
graphVisibilityState={graphVisibilityState}
|
||||
index={record.index}
|
||||
disabled={isGraphDisabled}
|
||||
checkBoxOnChangeHandler={(_e, idx): void => onToggleSeriesOnOff(idx)}
|
||||
/>
|
||||
),
|
||||
@@ -50,6 +53,7 @@ export function getChartManagerColumns({
|
||||
<SeriesLabel
|
||||
label={label ?? ''}
|
||||
labelIndex={record.index}
|
||||
disabled={isGraphDisabled}
|
||||
onClick={onToggleSeriesVisibility}
|
||||
/>
|
||||
),
|
||||
@@ -1,9 +1,9 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { usePanelContextMenu } from 'container/WidgetCard/Panels/hooks/usePanelContextMenu';
|
||||
import { usePanelContextMenu } from '../usePanelContextMenu';
|
||||
|
||||
// The hook composes `useCoordinates` (popover state) and `useGraphContextMenu`
|
||||
// (menu items). We mock both so the test focuses on the `enableDrillDown` gate
|
||||
@@ -37,7 +37,7 @@ jest.mock('container/QueryTable/Drilldown/drilldownUtils', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('container/WidgetCard/Panels/utils', () => ({
|
||||
jest.mock('container/PanelWrapper/utils', () => ({
|
||||
isApmMetric: jest.fn(() => false),
|
||||
getTimeRangeFromStepInterval: jest.fn(() => ({ start: 0, end: 0 })),
|
||||
}));
|
||||
@@ -2,13 +2,15 @@ import { act, renderHook } from '@testing-library/react';
|
||||
import {
|
||||
getStoredSeriesVisibility,
|
||||
updateSeriesVisibilityToLocalStorage,
|
||||
} from 'lib/visualization/panels/utils/legendVisibilityUtils';
|
||||
} from 'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils';
|
||||
import type { MouseEvent } from 'react';
|
||||
|
||||
import { PieSlice } from 'lib/visualization/charts/types';
|
||||
import { usePieInteractions } from 'lib/visualization/hooks/usePieInteractions';
|
||||
import { PieSlice } from '../../charts/types';
|
||||
import { usePieInteractions } from '../usePieInteractions';
|
||||
|
||||
jest.mock('lib/visualization/panels/utils/legendVisibilityUtils');
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',
|
||||
);
|
||||
|
||||
const mockGetStored = getStoredSeriesVisibility as jest.MockedFunction<
|
||||
typeof getStoredSeriesVisibility
|
||||
@@ -1,7 +1,7 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useScrollToWidgetIdStore } from 'providers/Dashboard/helpers/scrollToWidgetIdHelper';
|
||||
|
||||
import { useScrollWidgetIntoView } from 'lib/visualization/hooks/useScrollWidgetIntoView';
|
||||
import { useScrollWidgetIntoView } from '../useScrollWidgetIntoView';
|
||||
|
||||
jest.mock('providers/Dashboard/helpers/scrollToWidgetIdHelper');
|
||||
|
||||
@@ -3,14 +3,14 @@ import { UseQueryResult } from 'react-query';
|
||||
import {
|
||||
getTimeRangeFromStepInterval,
|
||||
isApmMetric,
|
||||
} from 'container/WidgetCard/Panels/utils';
|
||||
} from 'container/PanelWrapper/utils';
|
||||
import { getUplotClickData } from 'container/QueryTable/Drilldown/drilldownUtils';
|
||||
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
|
||||
import {
|
||||
PopoverPosition,
|
||||
useCoordinates,
|
||||
} from 'periscope/components/ContextMenu';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
getStoredSeriesVisibility,
|
||||
updateSeriesVisibilityToLocalStorage,
|
||||
} from 'lib/visualization/panels/utils/legendVisibilityUtils';
|
||||
import { PieSlice } from 'lib/visualization/charts/types';
|
||||
} from '../panels/utils/legendVisibilityUtils';
|
||||
import { PieSlice } from '../charts/types';
|
||||
|
||||
export interface UsePieInteractionsResult {
|
||||
/** The hovered/focused slice (drives donut dimming + tooltip). */
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useMemo } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { calculateChartDimensions } from 'lib/visualization/charts/utils';
|
||||
import { calculateChartDimensions } from 'container/DashboardContainer/visualization/charts/utils';
|
||||
import { MAX_LEGEND_WIDTH } from 'lib/uPlotV2/components/Legend/Legend';
|
||||
import { LegendConfig, LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
|
||||
import 'lib/visualization/layout/ChartLayout/ChartLayout.styles.scss';
|
||||
import './ChartLayout.styles.scss';
|
||||
|
||||
export interface ChartLayoutProps {
|
||||
showLegend?: boolean;
|
||||
@@ -1,28 +1,26 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { PanelWrapperProps } from 'container/WidgetCard/Panels/panelWrapper.types';
|
||||
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
|
||||
import { useDashboardCursorSyncMode } from 'hooks/dashboard/useDashboardCursorSyncMode';
|
||||
import { useSyncTooltipFilterMode } from 'hooks/dashboard/useSyncTooltipFilterMode';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import {
|
||||
IRenderTooltipFooterArgs,
|
||||
LegendPosition,
|
||||
} from 'lib/uPlotV2/components/types';
|
||||
import {
|
||||
DashboardCursorSync,
|
||||
SyncTooltipFilterMode,
|
||||
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
import ContextMenu from 'periscope/components/ContextMenu';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import uPlot from 'uplot';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import ChartManager from 'lib/visualization/components/ChartManager/ChartManager';
|
||||
import { usePanelContextMenu } from 'container/WidgetCard/Panels/hooks/usePanelContextMenu';
|
||||
import { PanelMode } from 'lib/visualization/panels/types';
|
||||
import { prepareBarPanelConfig } from 'container/WidgetCard/Panels/BarPanel/utils';
|
||||
import BarChart from '../../charts/BarChart/BarChart';
|
||||
import ChartManager from '../../components/ChartManager/ChartManager';
|
||||
import { usePanelContextMenu } from '../../hooks/usePanelContextMenu';
|
||||
import { prepareBarPanelConfig } from './utils';
|
||||
|
||||
import 'container/WidgetCard/Panels/Panel.styles.scss';
|
||||
import TooltipFooter from 'lib/visualization/panels/components/TooltipFooter';
|
||||
import '../Panel.styles.scss';
|
||||
import TooltipFooter from '../components/TooltipFooter';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
import { StackMode } from 'lib/uPlotV2/config/types';
|
||||
|
||||
@@ -46,12 +44,9 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
// These panels never render inside a dashboard, so there is no stored
|
||||
// cursor-sync preference to read — only the panel-mode gate applies.
|
||||
const syncMode =
|
||||
panelMode === PanelMode.DASHBOARD_VIEW
|
||||
? DashboardCursorSync.Crosshair
|
||||
: DashboardCursorSync.None;
|
||||
const dashboardId = useDashboardStore((s) => s.dashboardData?.id);
|
||||
const [syncMode] = useDashboardCursorSyncMode(dashboardId, panelMode);
|
||||
const [syncFilterMode] = useSyncTooltipFilterMode(dashboardId);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
@@ -152,6 +147,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
<div className="panel-container" ref={graphRef}>
|
||||
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
|
||||
<BarChart
|
||||
key={`${syncMode}-${syncFilterMode}`}
|
||||
stack={widget.stackedBarChart ? StackMode.Normal : StackMode.None}
|
||||
config={config}
|
||||
legendConfig={{
|
||||
@@ -169,7 +165,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
decimalPrecision={widget.decimalPrecision}
|
||||
timezone={timezone}
|
||||
syncMode={syncMode}
|
||||
syncFilterMode={SyncTooltipFilterMode.Filtered}
|
||||
syncFilterMode={syncFilterMode}
|
||||
renderTooltipFooter={renderTooltipFooter}
|
||||
>
|
||||
<ContextMenu
|
||||
@@ -1,17 +1,20 @@
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import {
|
||||
MetricRangePayloadProps,
|
||||
MetricRangePayloadV3,
|
||||
} from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { PanelMode } from 'lib/visualization/panels/types';
|
||||
import { prepareBarPanelConfig } from 'container/WidgetCard/Panels/BarPanel/utils';
|
||||
import { PanelMode } from '../../types';
|
||||
import { prepareBarPanelConfig } from '../utils';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
|
||||
jest.mock('lib/visualization/panels/utils/legendVisibilityUtils', () => ({
|
||||
getStoredSeriesVisibility: jest.fn(),
|
||||
}));
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/panels/utils/legendVisibilityUtils',
|
||||
() => ({
|
||||
getStoredSeriesVisibility: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('lib/uPlotLib/plugins/onClickPlugin', () => ({
|
||||
__esModule: true,
|
||||
@@ -7,12 +7,12 @@ import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { DrawStyle } from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { get } from 'lodash-es';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { PanelMode } from 'lib/visualization/panels/types';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { PanelMode } from '../types';
|
||||
import { buildBaseConfig } from '../utils/baseConfigBuilder';
|
||||
|
||||
export function prepareBarPanelConfig({
|
||||
widget,
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { PanelWrapperProps } from 'container/WidgetCard/Panels/panelWrapper.types';
|
||||
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import {
|
||||
@@ -8,15 +8,15 @@ import {
|
||||
} from 'lib/uPlotV2/components/types';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import Histogram from 'lib/visualization/charts/Histogram/Histogram';
|
||||
import ChartManager from 'lib/visualization/components/ChartManager/ChartManager';
|
||||
import Histogram from '../../charts/Histogram/Histogram';
|
||||
import ChartManager from '../../components/ChartManager/ChartManager';
|
||||
import {
|
||||
prepareHistogramPanelConfig,
|
||||
prepareHistogramPanelData,
|
||||
} from 'container/WidgetCard/Panels/HistogramPanel/utils';
|
||||
} from './utils';
|
||||
|
||||
import 'container/WidgetCard/Panels/Panel.styles.scss';
|
||||
import TooltipFooter from 'lib/visualization/panels/components/TooltipFooter';
|
||||
import '../Panel.styles.scss';
|
||||
import TooltipFooter from '../components/TooltipFooter';
|
||||
|
||||
function HistogramPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const {
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { UseQueryResult } from 'react-query';
|
||||
import { HistogramChartProps } from 'lib/visualization/charts/types';
|
||||
import { PanelMode } from 'lib/visualization/panels/types';
|
||||
import { HistogramChartProps } from 'container/DashboardContainer/visualization/charts/types';
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import {
|
||||
MetricQueryRangeSuccessResponse,
|
||||
MetricRangePayloadProps,
|
||||
} from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import HistogramPanel from 'container/WidgetCard/Panels/HistogramPanel/HistogramPanel';
|
||||
import HistogramPanel from '../HistogramPanel';
|
||||
|
||||
jest.mock('hooks/useDimensions', () => ({
|
||||
useResizeObserver: jest.fn().mockReturnValue({ width: 800, height: 400 }),
|
||||
@@ -31,33 +31,42 @@ jest.mock('providers/Timezone', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('lib/visualization/hooks/useScrollWidgetIntoView', () => ({
|
||||
useScrollWidgetIntoView: jest.fn(),
|
||||
}));
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/hooks/useScrollWidgetIntoView',
|
||||
() => ({
|
||||
useScrollWidgetIntoView: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('lib/visualization/charts/Histogram/Histogram', () => ({
|
||||
__esModule: true,
|
||||
default: (props: HistogramChartProps): JSX.Element => (
|
||||
<div data-testid="histogram-chart">
|
||||
<div data-testid="histogram-props">
|
||||
{JSON.stringify({
|
||||
legendPosition: props.legendConfig?.position,
|
||||
isQueriesMerged: props.isQueriesMerged,
|
||||
yAxisUnit: props?.yAxisUnit,
|
||||
decimalPrecision: props?.decimalPrecision,
|
||||
})}
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/Histogram/Histogram',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: (props: HistogramChartProps): JSX.Element => (
|
||||
<div data-testid="histogram-chart">
|
||||
<div data-testid="histogram-props">
|
||||
{JSON.stringify({
|
||||
legendPosition: props.legendConfig?.position,
|
||||
isQueriesMerged: props.isQueriesMerged,
|
||||
yAxisUnit: props?.yAxisUnit,
|
||||
decimalPrecision: props?.decimalPrecision,
|
||||
})}
|
||||
</div>
|
||||
{props.layoutChildren}
|
||||
</div>
|
||||
{props.layoutChildren}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
jest.mock('lib/visualization/components/ChartManager/ChartManager', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="chart-manager">ChartManager</div>
|
||||
),
|
||||
}));
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/components/ChartManager/ChartManager',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="chart-manager">ChartManager</div>
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
function createQueryResponse(
|
||||
payloadOverrides: Partial<MetricRangePayloadProps> = {},
|
||||
@@ -1,23 +1,23 @@
|
||||
import { histogramBucketSizes } from '@grafana/data';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { DEFAULT_BUCKET_COUNT } from 'lib/visualization/constants';
|
||||
import { DEFAULT_BUCKET_COUNT } from 'container/PanelWrapper/constants';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { DrawStyle } from 'lib/uPlotV2/config/types';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { AlignedData } from 'uplot';
|
||||
import { incrRoundDn, roundDecimals } from 'utils/round';
|
||||
|
||||
import { PanelMode } from 'lib/visualization/panels/types';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { PanelMode } from '../types';
|
||||
import { buildBaseConfig } from '../utils/baseConfigBuilder';
|
||||
import {
|
||||
buildHistogramBuckets,
|
||||
mergeAlignedDataTables,
|
||||
prependNullBinToFirstHistogramSeries,
|
||||
replaceUndefinedWithNullInAlignedData,
|
||||
} from 'lib/visualization/panels/utils/histogram';
|
||||
} from '../utils/histogram';
|
||||
|
||||
export interface PrepareHistogramPanelDataParams {
|
||||
apiResponse: MetricRangePayloadProps;
|
||||
@@ -1,28 +1,26 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import TimeSeries from 'lib/visualization/charts/TimeSeries/TimeSeries';
|
||||
import ChartManager from 'lib/visualization/components/ChartManager/ChartManager';
|
||||
import { usePanelContextMenu } from 'container/WidgetCard/Panels/hooks/usePanelContextMenu';
|
||||
import { PanelWrapperProps } from 'container/WidgetCard/Panels/panelWrapper.types';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import ChartManager from 'container/DashboardContainer/visualization/components/ChartManager/ChartManager';
|
||||
import { usePanelContextMenu } from 'container/DashboardContainer/visualization/hooks/usePanelContextMenu';
|
||||
import { PanelWrapperProps } from 'container/PanelWrapper/panelWrapper.types';
|
||||
import { useDashboardCursorSyncMode } from 'hooks/dashboard/useDashboardCursorSyncMode';
|
||||
import { useSyncTooltipFilterMode } from 'hooks/dashboard/useSyncTooltipFilterMode';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import {
|
||||
IRenderTooltipFooterArgs,
|
||||
LegendPosition,
|
||||
} from 'lib/uPlotV2/components/types';
|
||||
import {
|
||||
DashboardCursorSync,
|
||||
SyncTooltipFilterMode,
|
||||
} from 'lib/uPlotV2/plugins/TooltipPlugin/types';
|
||||
import { ContextMenu } from 'periscope/components/ContextMenu';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import uPlot from 'uplot';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
|
||||
import { prepareUPlotConfig } from 'container/WidgetCard/Panels/TimeSeriesPanel/utils';
|
||||
import { PanelMode } from 'lib/visualization/panels/types';
|
||||
import { prepareUPlotConfig } from '../TimeSeriesPanel/utils';
|
||||
|
||||
import 'container/WidgetCard/Panels/Panel.styles.scss';
|
||||
import TooltipFooter from 'lib/visualization/panels/components/TooltipFooter';
|
||||
import '../Panel.styles.scss';
|
||||
import TooltipFooter from '../components/TooltipFooter';
|
||||
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
||||
|
||||
function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
@@ -44,12 +42,9 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const { timezone } = useTimezone();
|
||||
|
||||
// These panels never render inside a dashboard, so there is no stored
|
||||
// cursor-sync preference to read — only the panel-mode gate applies.
|
||||
const syncMode =
|
||||
panelMode === PanelMode.DASHBOARD_VIEW
|
||||
? DashboardCursorSync.Crosshair
|
||||
: DashboardCursorSync.None;
|
||||
const dashboardId = useDashboardStore((s) => s.dashboardData?.id);
|
||||
const [syncMode] = useDashboardCursorSyncMode(dashboardId, panelMode);
|
||||
const [syncFilterMode] = useSyncTooltipFilterMode(dashboardId);
|
||||
|
||||
useEffect((): void => {
|
||||
const { startTime, endTime } = getTimeRange(queryResponse);
|
||||
@@ -100,6 +95,11 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
minTimeScale,
|
||||
maxTimeScale,
|
||||
timezone,
|
||||
// `config` gets mutated by TooltipPlugin (config.setCursor for cursor sync).
|
||||
// Rebuild it on syncMode changes so the new chart instance starts from a
|
||||
// clean config — otherwise switching to "No Sync" would inherit stale sync
|
||||
// settings from the previous mode.
|
||||
syncMode,
|
||||
]);
|
||||
|
||||
const layoutChildren = useMemo(() => {
|
||||
@@ -137,6 +137,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
<div className="panel-container" ref={graphRef}>
|
||||
{containerDimensions.width > 0 && containerDimensions.height > 0 && (
|
||||
<TimeSeries
|
||||
key={`${syncMode}-${syncFilterMode}`}
|
||||
config={config}
|
||||
legendConfig={{
|
||||
position: widget?.legendPosition ?? LegendPosition.BOTTOM,
|
||||
@@ -150,7 +151,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
width={containerDimensions.width}
|
||||
height={containerDimensions.height}
|
||||
syncMode={syncMode}
|
||||
syncFilterMode={SyncTooltipFilterMode.Filtered}
|
||||
syncFilterMode={syncFilterMode}
|
||||
layoutChildren={layoutChildren}
|
||||
renderTooltipFooter={renderTooltipFooter}
|
||||
>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user