mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-05 11:00:41 +01:00
Compare commits
10 Commits
feat/googl
...
fixes/dash
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58a7a6724c | ||
|
|
b0d068a924 | ||
|
|
e0da06f76d | ||
|
|
85f9924b4b | ||
|
|
c015622258 | ||
|
|
0f36cb9334 | ||
|
|
e1ee386016 | ||
|
|
717d37a945 | ||
|
|
63b130ccd1 | ||
|
|
618031ddaa |
28
.github/CODEOWNERS
vendored
28
.github/CODEOWNERS
vendored
@@ -152,39 +152,29 @@ go.mod @therealpandey
|
||||
|
||||
## Dashboard Types
|
||||
|
||||
/frontend/src/api/types/dashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/types/api/dashboard/ @SigNoz/pulse-frontend
|
||||
/frontend/src/types/api/widgets/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard List
|
||||
## Widget Card
|
||||
|
||||
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/ListOfDashboard/ @SigNoz/pulse-frontend
|
||||
|
||||
# Dashboard Widget Page
|
||||
|
||||
/frontend/src/pages/DashboardWidget/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/NewWidget/ @SigNoz/pulse-frontend
|
||||
|
||||
## Dashboard Page
|
||||
|
||||
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/DashboardContainer/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/GridCardLayout/ @SigNoz/pulse-frontend
|
||||
/frontend/src/container/WidgetCard/ @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 V2
|
||||
/frontend/src/pages/DashboardPageV2/ @SigNoz/pulse-frontend
|
||||
/frontend/src/pages/DashboardsListPageV2/ @SigNoz/pulse-frontend
|
||||
## Dashboard Pages
|
||||
|
||||
/frontend/src/pages/DashboardPage/ @SigNoz/pulse-frontend
|
||||
/frontend/src/pages/DashboardsListPage/ @SigNoz/pulse-frontend
|
||||
|
||||
## Infrastructure Monitoring
|
||||
/frontend/src/pages/InfrastructureMonitoring/ @SigNoz/pulse-frontend
|
||||
|
||||
@@ -44,6 +44,8 @@ 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"
|
||||
@@ -87,6 +89,9 @@ 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,6 +28,7 @@ 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"
|
||||
@@ -60,6 +61,7 @@ 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"
|
||||
@@ -103,6 +105,9 @@ 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,6 +9217,116 @@ 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:
|
||||
@@ -14441,6 +14551,197 @@ 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
|
||||
|
||||
95
ee/subscription/httpsubscription/provider.go
Normal file
95
ee/subscription/httpsubscription/provider.go
Normal file
@@ -0,0 +1,95 @@
|
||||
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,9 +323,10 @@
|
||||
"name": "react",
|
||||
"importNames": [
|
||||
"createContext",
|
||||
"useContext"
|
||||
"useContext",
|
||||
"useSyncExternalStore"
|
||||
],
|
||||
"message": "[State mgmt] React Context is deprecated. Migrate shared state to Zustand."
|
||||
"message": "[State mgmt] React Context and hand-rolled external stores are deprecated. Migrate shared state to Zustand."
|
||||
},
|
||||
{
|
||||
"name": "immer",
|
||||
@@ -565,12 +566,12 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
// Root V2 pages own the dashboard fetch lifecycle; useDashboardFetchRequired wraps it.
|
||||
// Root dashboard pages own the fetch lifecycle; useDashboardFetchRequired wraps it.
|
||||
// Everywhere else must use useDashboardFetchRequired().
|
||||
"files": [
|
||||
"src/pages/DashboardPageV2/DashboardPageV2.tsx",
|
||||
"src/pages/DashboardPageV2/PanelEditorPage/PanelEditorPage.tsx",
|
||||
"src/pages/DashboardPageV2/DashboardContainer/hooks/useDashboardFetchRequired.ts"
|
||||
"src/pages/DashboardPage/DashboardPage.tsx",
|
||||
"src/pages/DashboardPage/PanelEditorPage/PanelEditorPage.tsx",
|
||||
"src/pages/DashboardPage/DashboardContainer/hooks/useDashboardFetchRequired.ts"
|
||||
],
|
||||
"rules": {
|
||||
"signoz/no-dashboard-fetch-outside-root": "off"
|
||||
|
||||
@@ -94,23 +94,18 @@ export const OnboardingV2 = Loadable(
|
||||
export const DashboardsListPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPageV2'
|
||||
/* webpackChunkName: "DashboardsListPage" */ 'pages/DashboardsListPage'
|
||||
),
|
||||
);
|
||||
|
||||
export const DashboardPage = Loadable(
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPageV2'),
|
||||
);
|
||||
|
||||
export const DashboardWidget = Loadable(
|
||||
() =>
|
||||
import(/* webpackChunkName: "DashboardWidgetPage" */ 'pages/DashboardWidget'),
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ 'pages/DashboardPage'),
|
||||
);
|
||||
|
||||
export const DashboardPanelEditorPage = Loadable(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "DashboardPanelEditorPage" */ 'pages/DashboardPageV2/PanelEditorPage/PanelEditorPage'
|
||||
/* webpackChunkName: "DashboardPanelEditorPage" */ 'pages/DashboardPage/PanelEditorPage/PanelEditorPage'
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
DashboardPage,
|
||||
DashboardPanelEditorPage,
|
||||
DashboardsListPage,
|
||||
DashboardWidget,
|
||||
EditRulesPage,
|
||||
ErrorDetails,
|
||||
ForgotPassword,
|
||||
@@ -183,13 +182,6 @@ 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,
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
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;
|
||||
@@ -1,34 +0,0 @@
|
||||
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,6 +10522,153 @@ 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[];
|
||||
};
|
||||
@@ -11740,6 +11887,30 @@ 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;
|
||||
};
|
||||
|
||||
280
frontend/src/api/generated/services/subscriptions/index.ts
Normal file
280
frontend/src/api/generated/services/subscriptions/index.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* ! 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));
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
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;
|
||||
@@ -1,23 +0,0 @@
|
||||
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,6 +6,7 @@ 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,
|
||||
@@ -545,20 +546,22 @@ 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,
|
||||
dynamicVariables,
|
||||
}: GetQueryResultsProps): PrepareQueryRangePayloadV5Result => {
|
||||
export const prepareQueryRangePayloadV5 = (
|
||||
{
|
||||
query,
|
||||
globalSelectedInterval,
|
||||
graphType,
|
||||
selectedTime,
|
||||
tableParams,
|
||||
variables = {},
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
formatForWeb,
|
||||
originalGraphType,
|
||||
fillGaps,
|
||||
}: GetQueryResultsProps,
|
||||
dynamicVariables: DynamicVariableSuggestion[] = [],
|
||||
): PrepareQueryRangePayloadV5Result => {
|
||||
let legendMap: Record<string, string> = {};
|
||||
const requestType = mapPanelTypeToRequestType(graphType);
|
||||
let queries: QueryEnvelope[] = [];
|
||||
@@ -671,9 +674,9 @@ export const prepareQueryRangePayloadV5 = ({
|
||||
(acc, [key, value]) => {
|
||||
acc[key] = {
|
||||
value,
|
||||
type: dynamicVariables
|
||||
?.find((v) => v.name === key)
|
||||
?.type?.toLowerCase() as VariableType,
|
||||
type: dynamicVariables.some((v) => v.name === key)
|
||||
? ('dynamic' as VariableType)
|
||||
: undefined,
|
||||
};
|
||||
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/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
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/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card } from 'container/WidgetCard/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/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
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/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card } from 'container/WidgetCard/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/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
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/GridCardLayout/styles';
|
||||
import { CardContainer } from 'container/WidgetCard/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/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
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/GridCardLayout/config';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card } from 'container/GridCardLayout/styles';
|
||||
import { ViewMenuAction } from 'container/WidgetCard/config';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card } from 'container/WidgetCard/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/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
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/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -4,10 +4,9 @@ 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/GridCardLayout/useResolveQuery';
|
||||
import useUpdatedQuery from 'container/WidgetCard/hooks/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';
|
||||
@@ -80,7 +79,6 @@ export function useNavigateToExplorer(): (
|
||||
);
|
||||
|
||||
const { getUpdatedQuery } = useUpdatedQuery();
|
||||
const { dashboardData } = useDashboardStore();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
return useCallback(
|
||||
@@ -112,7 +110,6 @@ export function useNavigateToExplorer(): (
|
||||
panelTypes: PANEL_TYPES.TIME_SERIES,
|
||||
timePreferance: 'GLOBAL_TIME',
|
||||
},
|
||||
dashboardData,
|
||||
})
|
||||
.then((query) => {
|
||||
preparedQuery = query;
|
||||
@@ -136,13 +133,6 @@ export function useNavigateToExplorer(): (
|
||||
|
||||
window.open(withBasePath(newExplorerPath), sameTab ? '_self' : '_blank');
|
||||
},
|
||||
[
|
||||
prepareQuery,
|
||||
minTime,
|
||||
maxTime,
|
||||
getUpdatedQuery,
|
||||
dashboardData,
|
||||
notifications,
|
||||
],
|
||||
[prepareQuery, minTime, maxTime, getUpdatedQuery, 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 'container/GridPanelSwitch/utils';
|
||||
import { generateGridTitle } from 'utils/generateGridTitle';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Spline } from '@signozhq/icons';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import QueryTypeTag from '../QueryTypeTag';
|
||||
import QueryTypeTag from 'components/QueryTypeTag/QueryTypeTag';
|
||||
|
||||
interface IPlotTagProps {
|
||||
queryType: EQueryType;
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
QUERY_BUILDER_OPERATORS_BY_KEY_TYPE,
|
||||
queryOperatorSuggestions,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { useDashboardVariablesByType } from 'hooks/dashboard/useDashboardVariablesByType';
|
||||
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { debounce, isNull } from 'lodash-es';
|
||||
@@ -258,10 +258,7 @@ function QuerySearch({
|
||||
const lastValueRef = useRef<string>('');
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
const dashboardDynamicVariables = useDashboardVariablesByType(
|
||||
'DYNAMIC',
|
||||
'values',
|
||||
);
|
||||
const dashboardDynamicVariables = useDynamicVariableSuggestions();
|
||||
|
||||
// Add back the generateOptions function and useEffect
|
||||
const generateOptions = (keys: {
|
||||
@@ -1188,8 +1185,8 @@ function QuerySearch({
|
||||
);
|
||||
|
||||
// Add dynamic variables suggestions for the current key
|
||||
const variableName = dashboardDynamicVariables?.find(
|
||||
(variable) => variable?.dynamicVariablesAttribute === keyName,
|
||||
const variableName = dashboardDynamicVariables.find(
|
||||
(variable) => variable.attribute === keyName,
|
||||
)?.name;
|
||||
|
||||
if (variableName) {
|
||||
|
||||
@@ -17,12 +17,6 @@ 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,12 +21,6 @@ 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 {
|
||||
@@ -152,15 +146,16 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// 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: '' });
|
||||
// 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 },
|
||||
);
|
||||
});
|
||||
|
||||
it('calls provided onRun on Mod-Enter', async () => {
|
||||
|
||||
@@ -31,12 +31,6 @@ 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,6 +525,34 @@ 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.
|
||||
*
|
||||
@@ -542,12 +570,16 @@ export const convertFiltersToExpressionWithExistingQuery = (
|
||||
* - `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;
|
||||
@@ -557,6 +589,9 @@ 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>();
|
||||
@@ -658,6 +693,13 @@ 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
|
||||
|
||||
@@ -0,0 +1,526 @@
|
||||
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,5 +1,8 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
removeKeysFromExpression,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
@@ -10,13 +13,33 @@ import { cloneDeep, isArray } from 'lodash-es';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { isKeyMatch } from './utils';
|
||||
import { getKeySpellings, 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];
|
||||
|
||||
@@ -102,8 +125,8 @@ export function deriveCheckboxState({
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new query with every clause for this attribute key removed, both
|
||||
* from the structured filter items and the raw filter expression.
|
||||
* 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.
|
||||
*/
|
||||
export function clearFilterFromQuery({
|
||||
currentQuery,
|
||||
@@ -118,24 +141,28 @@ export function clearFilterFromQuery({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
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',
|
||||
},
|
||||
})),
|
||||
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',
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -194,12 +221,6 @@ 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(),
|
||||
@@ -267,12 +288,6 @@ 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
|
||||
@@ -309,9 +324,10 @@ export function applyCheckboxToggle({
|
||||
? currentFilter.value.includes(value)
|
||||
: currentFilter.value === value;
|
||||
|
||||
// When clicking unchecked "Other" item, user wants to SELECT it
|
||||
// Replace NOT IN filter with IN [value]
|
||||
if (previousState === 'unchecked' && checked) {
|
||||
// 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) {
|
||||
const newFilter: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getOperatorValue(OPERATORS.IN),
|
||||
@@ -324,12 +340,6 @@ 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)
|
||||
@@ -369,12 +379,6 @@ 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)) {
|
||||
@@ -384,16 +388,6 @@ 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),
|
||||
);
|
||||
@@ -456,6 +450,18 @@ 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,3 +39,16 @@ 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 'container/NewWidget/RightContainer/timeItems';
|
||||
import { timeItems } from 'constants/timePreference';
|
||||
|
||||
export const menuItems = timeItems.map((item) => ({
|
||||
key: item.enum,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import TimeItems, {
|
||||
timePreferance,
|
||||
timePreferenceType,
|
||||
} from 'container/NewWidget/RightContainer/timeItems';
|
||||
} from 'constants/timePreference';
|
||||
|
||||
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 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
|
||||
import { getBackgroundColorAndThresholdCheck } from './utils';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { evaluateThresholdWithConvertedValue } from 'container/GridTableComponent/utils';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { evaluateThresholdWithConvertedValue } from 'container/WidgetCard/Panels/TablePanel/utils';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
|
||||
function doesValueSatisfyThreshold(
|
||||
rawValue: number,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Uplot from 'components/Uplot';
|
||||
import GridTableComponent from 'container/GridTableComponent';
|
||||
import GridValueComponent from 'container/GridValueComponent';
|
||||
import GridTableComponent from 'container/WidgetCard/Panels/TablePanel';
|
||||
import GridValueComponent from 'container/WidgetCard/Panels/ValuePanel';
|
||||
import LogsPanelComponent from 'container/LogsPanelTable/LogsPanelComponent';
|
||||
import TracesTableComponent from 'container/TracesTableComponent/TracesTableComponent';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -16,7 +16,6 @@ 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/DashboardPageV2/DashboardContainer/PanelEditor/newPanelRoute';
|
||||
import { NEW_PANEL_ID } from 'pages/DashboardPage/DashboardContainer/PanelEditor/newPanelRoute';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
getAllEndpointsWidgetData,
|
||||
getGroupByFiltersFromGroupByValues,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
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/GridCardLayout/GridCard';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
|
||||
function MetricOverTimeGraph({
|
||||
widget,
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
getStatusCodeBarChartWidgetData,
|
||||
statusCodeWidgetInfo,
|
||||
} from 'container/ApiMonitoring/utils';
|
||||
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 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 { 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/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
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 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { buildBaseConfig } from 'lib/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/GridCardLayout/GridCard', () => ({
|
||||
jest.mock('container/WidgetCard/Card', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn().mockImplementation(({ customOnRowClick }) => (
|
||||
<div data-testid="grid-card-mock">
|
||||
|
||||
@@ -21,15 +21,12 @@ interface MockQueryResult {
|
||||
}
|
||||
|
||||
// Mocks
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/BarChart/BarChart',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: jest
|
||||
.fn()
|
||||
.mockImplementation(() => <div data-testid="bar-chart-mock" />),
|
||||
}),
|
||||
);
|
||||
jest.mock('lib/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 } => ({
|
||||
@@ -43,7 +40,7 @@ jest.mock('components/CeleryTask/useNavigateToExplorer', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/GridCardLayout/useGraphClickToShowButton', () => ({
|
||||
jest.mock('container/WidgetCard/hooks/useGraphClickToShowButton', () => ({
|
||||
useGraphClickToShowButton: (): {
|
||||
componentClick: boolean;
|
||||
htmlRef: HTMLElement | null;
|
||||
@@ -53,7 +50,7 @@ jest.mock('container/GridCardLayout/useGraphClickToShowButton', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/GridCardLayout/useNavigateToExplorerPages', () => ({
|
||||
jest.mock('container/WidgetCard/hooks/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/GridCardLayout/useNavigateToExplorerPages';
|
||||
import { GraphClickMetaData } from 'container/WidgetCard/hooks/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/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
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 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import BarChart from 'lib/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 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { buildBaseConfig } from 'lib/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('container/NewWidget/RightContainer/alertFomatCategories', () => ({
|
||||
jest.mock('constants/formats/alertFormatCategories', () => ({
|
||||
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 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||
import PlotTag from 'components/PlotTag/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(
|
||||
'container/NewWidget/LeftContainer/WidgetGraph/PlotTag',
|
||||
'components/PlotTag/PlotTag',
|
||||
() =>
|
||||
function MockPlotTag(props: any): JSX.Element {
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useHistory } from 'react-router-dom';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { MOCK_QUERY } from 'container/QueryTable/Drilldown/__tests__/mockTableData';
|
||||
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import {
|
||||
defaultFeatureFlags,
|
||||
@@ -13,15 +12,11 @@ import {
|
||||
} from 'tests/test-utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import { v4 } from 'uuid';
|
||||
import { buildExportPanelLink } from 'pages/DashboardPage/DashboardContainer/PanelEditor/newPanelRoute';
|
||||
|
||||
import ExplorerOptionWrapper from '../ExplorerOptionWrapper';
|
||||
import { getExplorerToolBarVisibility } from '../utils';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('hooks/dashboard/useUpdateDashboard');
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useHistory: jest.fn(),
|
||||
@@ -40,7 +35,6 @@ const mockGetExplorerToolBarVisibility = jest.mocked(
|
||||
getExplorerToolBarVisibility,
|
||||
);
|
||||
|
||||
const mockUseUpdateDashboard = jest.mocked(useUpdateDashboard);
|
||||
const mockUseHistory = jest.mocked(useHistory);
|
||||
|
||||
// Mock data
|
||||
@@ -143,17 +137,6 @@ describe('ExplorerOptionWrapper', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockGetExplorerToolBarVisibility.mockReturnValue(true);
|
||||
// Mock useUpdateDashboard to return a mutation object
|
||||
mockUseUpdateDashboard.mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
mutateAsync: jest.fn(),
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
isSuccess: false,
|
||||
data: undefined,
|
||||
error: null,
|
||||
reset: jest.fn(),
|
||||
} as unknown as ReturnType<typeof useUpdateDashboard>);
|
||||
});
|
||||
|
||||
it('should navigate to alert creation page when "Create an Alert" is clicked in logs-explorer', async () => {
|
||||
@@ -291,34 +274,27 @@ describe('ExplorerOptionWrapper', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should test actual handleExport function with generateExportToDashboardLink and verify useUpdateDashboard is NOT called', async () => {
|
||||
it('should navigate to the panel editor via the export link without writing the dashboard', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
// Mock the safeNavigate function
|
||||
const mockSafeNavigate = jest.fn();
|
||||
|
||||
// Get the mock mutate function to track calls
|
||||
const mockMutate = mockUseUpdateDashboard().mutate as jest.MockedFunction<
|
||||
(...args: unknown[]) => void
|
||||
>;
|
||||
|
||||
const panelTypeParam = PANEL_TYPES.TIME_SERIES;
|
||||
const widgetId = v4();
|
||||
const query = mockQuery;
|
||||
|
||||
// Create a real handleExport function similar to LogsExplorerViews
|
||||
// This should NOT call useUpdateDashboard (as per PR #8029)
|
||||
// Export navigates only; it must not write the dashboard (PR #8029).
|
||||
const handleExport = (dashboard: ExportDashboard | null): void => {
|
||||
if (!dashboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Call the actual generateExportToDashboardLink function (not mocked)
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
// Call the real link builder (not mocked)
|
||||
const dashboardEditView = buildExportPanelLink({
|
||||
query,
|
||||
panelType: panelTypeParam,
|
||||
dashboardId: dashboard.id,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
// Simulate navigation
|
||||
@@ -379,15 +355,13 @@ describe('ExplorerOptionWrapper', () => {
|
||||
// Wait for the handleExport function to be called and navigation to occur
|
||||
await waitFor(() => {
|
||||
expect(mockSafeNavigate).toHaveBeenCalledTimes(1);
|
||||
// V2 panel-editor link: compositeQuery is double-encoded (see newPanelRoute).
|
||||
expect(mockSafeNavigate).toHaveBeenCalledWith(
|
||||
`/dashboard/${TEST_DASHBOARD_ID}/new?graphType=${panelTypeParam}&widgetId=${widgetId}&compositeQuery=${encodeURIComponent(
|
||||
JSON.stringify(query),
|
||||
`/dashboard/${TEST_DASHBOARD_ID}/panel/new?panelKind=signoz%2FTimeSeriesPanel&compositeQuery=${encodeURIComponent(
|
||||
encodeURIComponent(JSON.stringify(query)),
|
||||
)}`,
|
||||
);
|
||||
});
|
||||
|
||||
// Assert that useUpdateDashboard was NOT called (as per PR #8029)
|
||||
expect(mockMutate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import ClickHouseQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse/query';
|
||||
import ClickHouseQueryBuilder from 'container/QueryBuilder/rawQueryEditors/ClickHouse/query';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import DOCLINKS from 'utils/docLinks';
|
||||
|
||||
import 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/ClickHouse/ClickHouse.styles.scss';
|
||||
import 'container/QueryBuilder/rawQueryEditors/ClickHouse/ClickHouse.styles.scss';
|
||||
|
||||
const ALERT_TYPE_DOC_LINK: Partial<Record<AlertTypes, string>> = {
|
||||
[AlertTypes.LOGS_BASED_ALERT]: DOCLINKS.QUERY_CLICKHOUSE_LOGS,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import TimeSeries from 'lib/visualization/charts/TimeSeries/TimeSeries';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
MiscellaneousFormats,
|
||||
ThroughputFormats,
|
||||
TimeFormats,
|
||||
} from 'container/NewWidget/RightContainer/types';
|
||||
} from 'constants/formats/types';
|
||||
|
||||
export const dataFormatConfig: Record<DataFormats, number> = {
|
||||
[DataFormats.BytesIEC]: 1,
|
||||
|
||||
@@ -15,9 +15,9 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import AnomalyAlertEvaluationView from 'container/AnomalyAlertEvaluationView';
|
||||
import { INITIAL_CRITICAL_THRESHOLD } from 'container/CreateAlertV2/context/constants';
|
||||
import { Threshold } from 'container/CreateAlertV2/context/types';
|
||||
import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util';
|
||||
import { getFormatNameByOptionId } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||
import { timePreferenceType } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { populateMultipleResults } from 'lib/query/populateMultipleResults';
|
||||
import { getFormatNameByOptionId } from 'constants/formats/alertFormatCategories';
|
||||
import { timePreferenceType } from 'constants/timePreference';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DataFormats } from 'container/NewWidget/RightContainer/types';
|
||||
import { DataFormats } from 'constants/formats/types';
|
||||
|
||||
import { covertIntoDataFormats } from './utils';
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import { ExecStats } from 'api/v5/v5';
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Threshold } from 'container/CreateAlertV2/context/types';
|
||||
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { PanelMode } from 'lib/visualization/panels/types';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
import {
|
||||
BooleanFormats,
|
||||
DataFormats,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
MiscellaneousFormats,
|
||||
ThroughputFormats,
|
||||
TimeFormats,
|
||||
} from 'container/NewWidget/RightContainer/types';
|
||||
} from 'constants/formats/types';
|
||||
import { TFunction } from 'i18next';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import PromQLQueryBuilder from 'container/NewWidget/LeftContainer/QuerySection/QueryBuilder/promQL/query';
|
||||
import PromQLQueryBuilder from 'container/QueryBuilder/rawQueryEditors/PromQL/query';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
function PromqlSection(): JSX.Element {
|
||||
|
||||
@@ -26,8 +26,8 @@ import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import QueryTypeTag from 'container/NewWidget/LeftContainer/QueryTypeTag';
|
||||
import PlotTag from 'container/NewWidget/LeftContainer/WidgetGraph/PlotTag';
|
||||
import QueryTypeTag from 'components/QueryTypeTag/QueryTypeTag';
|
||||
import PlotTag from 'components/PlotTag/PlotTag';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import useGetYAxisUnit from 'hooks/useGetYAxisUnit';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types';
|
||||
import { ThresholdProps } from 'types/api/widgets/threshold';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
const THRESHOLD_COLORS_SORTING_ORDER = ['Red', 'Orange', 'Green', 'Blue'];
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { FC, forwardRef, memo, useMemo } from 'react';
|
||||
import { ToggleGraphProps } from 'components/Graph/types';
|
||||
import { getComponentForPanelType } from 'constants/panelTypes';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GRID_TABLE_CONFIG } from 'container/GridTableComponent/config';
|
||||
|
||||
import { GridPanelSwitchProps, PropsTypePropsMap } from './types';
|
||||
|
||||
const GridPanelSwitch = forwardRef<
|
||||
ToggleGraphProps | undefined,
|
||||
GridPanelSwitchProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
panelType,
|
||||
data,
|
||||
yAxisUnit,
|
||||
panelData,
|
||||
query,
|
||||
options,
|
||||
thresholds,
|
||||
dataSource,
|
||||
},
|
||||
ref,
|
||||
): JSX.Element | null => {
|
||||
const currentProps: PropsTypePropsMap = useMemo(() => {
|
||||
const result: PropsTypePropsMap = {
|
||||
[PANEL_TYPES.TIME_SERIES]: {
|
||||
data,
|
||||
options,
|
||||
ref,
|
||||
},
|
||||
[PANEL_TYPES.VALUE]: {
|
||||
data,
|
||||
yAxisUnit,
|
||||
thresholds,
|
||||
},
|
||||
[PANEL_TYPES.TABLE]: {
|
||||
...GRID_TABLE_CONFIG,
|
||||
data: panelData,
|
||||
query,
|
||||
thresholds,
|
||||
sticky: true,
|
||||
},
|
||||
[PANEL_TYPES.LIST]: null,
|
||||
[PANEL_TYPES.PIE]: null,
|
||||
[PANEL_TYPES.TRACE]: null,
|
||||
[PANEL_TYPES.BAR]: {
|
||||
data,
|
||||
options,
|
||||
ref,
|
||||
},
|
||||
[PANEL_TYPES.HISTOGRAM]: null,
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null,
|
||||
};
|
||||
|
||||
return result;
|
||||
}, [data, options, ref, yAxisUnit, thresholds, panelData, query]);
|
||||
|
||||
const Component = getComponentForPanelType(panelType, dataSource) as FC<
|
||||
PropsTypePropsMap[typeof panelType]
|
||||
>;
|
||||
const componentProps = useMemo(
|
||||
() => currentProps[panelType],
|
||||
[panelType, currentProps],
|
||||
);
|
||||
|
||||
if (!Component || !componentProps) {
|
||||
return null;
|
||||
}
|
||||
return <Component {...componentProps} />;
|
||||
},
|
||||
);
|
||||
|
||||
GridPanelSwitch.displayName = 'GridPanelSwitch';
|
||||
|
||||
export default memo(GridPanelSwitch);
|
||||
@@ -1,48 +0,0 @@
|
||||
import { ForwardedRef } from 'react';
|
||||
import { StaticLineProps, ToggleGraphProps } from 'components/Graph/types';
|
||||
import { UplotProps } from 'components/Uplot/Uplot';
|
||||
import { GridTableComponentProps } from 'container/GridTableComponent/types';
|
||||
import { GridValueComponentProps } from 'container/GridValueComponent/types';
|
||||
import { timePreferance } from 'container/NewWidget/RightContainer/timeItems';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import uPlot from 'uplot';
|
||||
|
||||
import { PANEL_TYPES } from '../../constants/queryBuilder';
|
||||
|
||||
export type GridPanelSwitchProps = {
|
||||
panelType: PANEL_TYPES;
|
||||
data: uPlot.AlignedData;
|
||||
options: uPlot.Options;
|
||||
onClickHandler?: OnClickPluginOpts['onClick'];
|
||||
name: string;
|
||||
yAxisUnit?: string;
|
||||
staticLine?: StaticLineProps;
|
||||
onDragSelect?: (start: number, end: number) => void;
|
||||
panelData: QueryDataV3[];
|
||||
query: Query;
|
||||
thresholds?: Widgets['thresholds'];
|
||||
dataSource?: DataSource;
|
||||
selectedLogFields?: Widgets['selectedLogFields'];
|
||||
selectedTracesFields?: Widgets['selectedTracesFields'];
|
||||
selectedTime?: timePreferance;
|
||||
};
|
||||
|
||||
export type PropsTypePropsMap = {
|
||||
[PANEL_TYPES.TIME_SERIES]: UplotProps & {
|
||||
ref: ForwardedRef<ToggleGraphProps | undefined>;
|
||||
};
|
||||
[PANEL_TYPES.VALUE]: GridValueComponentProps;
|
||||
[PANEL_TYPES.TABLE]: GridTableComponentProps;
|
||||
[PANEL_TYPES.TRACE]: null;
|
||||
[PANEL_TYPES.PIE]: null;
|
||||
[PANEL_TYPES.LIST]: null;
|
||||
[PANEL_TYPES.BAR]: UplotProps & {
|
||||
ref: ForwardedRef<ToggleGraphProps | undefined>;
|
||||
};
|
||||
[PANEL_TYPES.HISTOGRAM]: null;
|
||||
[PANEL_TYPES.EMPTY_WIDGET]: null;
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import TimeSeries from 'lib/visualization/charts/TimeSeries/TimeSeries';
|
||||
import {
|
||||
IRenderTooltipFooterArgs,
|
||||
LegendPosition,
|
||||
|
||||
@@ -50,15 +50,12 @@ jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries',
|
||||
() => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="uplot-chart">TimeSeries Chart</div>
|
||||
),
|
||||
}),
|
||||
);
|
||||
jest.mock('lib/visualization/charts/TimeSeries/TimeSeries', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="uplot-chart">TimeSeries Chart</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('providers/Timezone', () => ({
|
||||
useTimezone: (): { timezone: { value: string } } => ({
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import {
|
||||
@@ -76,7 +75,7 @@ export function buildEntityMetricsChartConfig({
|
||||
show: true,
|
||||
side: 2,
|
||||
isDarkMode,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
isTimeAxis: true,
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
@@ -85,7 +84,6 @@ export function buildEntityMetricsChartConfig({
|
||||
side: 3,
|
||||
isDarkMode,
|
||||
yAxisUnit,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
});
|
||||
|
||||
if (!apiResponse?.data?.result) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import DashboardContainer from 'pages/DashboardPageV2/DashboardContainer';
|
||||
import DashboardContainer from 'pages/DashboardPage/DashboardContainer';
|
||||
|
||||
import { useSeededDashboardV2 } from './hooks/useSeededDashboardV2';
|
||||
import styles from './Overview.module.scss';
|
||||
|
||||
@@ -13,7 +13,7 @@ import LLMObservability from '../LLMObservability';
|
||||
// The Overview tab renders the full V2 DashboardContainer (toolbar + date picker
|
||||
// call useNavigationType, which needs a data router this integration test doesn't
|
||||
// set up). These cases assert tab routing, not dashboard rendering, so stub it.
|
||||
jest.mock('pages/DashboardPageV2/DashboardContainer', () => ({
|
||||
jest.mock('pages/DashboardPage/DashboardContainer', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div data-testid="llm-overview-dashboard" />,
|
||||
}));
|
||||
|
||||
@@ -92,12 +92,6 @@ 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: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import { colors } from 'lib/getRandomColor';
|
||||
|
||||
@@ -22,7 +22,7 @@ import { FlatLogData } from 'lib/logs/flatLogData';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
||||
|
||||
import { getLogPanelColumnsList } from './utils';
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import NewWidget from 'container/NewWidget';
|
||||
import { logsPaginationQueryRangeSuccessResponse } from 'mocks-server/__mockdata__/logs_query_range';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
|
||||
import i18n from 'ReactI18';
|
||||
import { act, fireEvent, render, screen, waitFor } from 'tests/test-utils';
|
||||
import { QueryRangePayload } from 'types/api/metrics/getQueryRange';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
// Constants
|
||||
const QUERY_RANGE_URL = `${ENVIRONMENT.baseURL}/api/v3/query_range`;
|
||||
const MOCK_SEARCH_PARAMS =
|
||||
'?graphType=list&widgetId=36a7b342-c642-4b92-abe4-cb833a244786&compositeQuery=%7B%22id%22%3A%22b325ac88-5e75-4117-a38c-1a2a7caf8115%22%2C%22builder%22%3A%7B%22queryData%22%3A%5B%7B%22dataSource%22%3A%22logs%22%2C%22queryName%22%3A%22A%22%2C%22aggregateOperator%22%3A%22noop%22%2C%22aggregateAttribute%22%3A%7B%22id%22%3A%22------%22%2C%22dataType%22%3A%22%22%2C%22key%22%3A%22%22%2C%22isColumn%22%3Afalse%2C%22type%22%3A%22%22%2C%22isJSON%22%3Afalse%7D%2C%22timeAggregation%22%3A%22rate%22%2C%22spaceAggregation%22%3A%22sum%22%2C%22functions%22%3A%5B%5D%2C%22filters%22%3A%7B%22items%22%3A%5B%5D%2C%22op%22%3A%22AND%22%7D%2C%22expression%22%3A%22A%22%2C%22disabled%22%3Afalse%2C%22stepInterval%22%3A60%2C%22having%22%3A%5B%5D%2C%22limit%22%3Anull%2C%22orderBy%22%3A%5B%7B%22columnName%22%3A%22timestamp%22%2C%22order%22%3A%22desc%22%7D%5D%2C%22groupBy%22%3A%5B%5D%2C%22legend%22%3A%22%22%2C%22reduceTo%22%3A%22avg%22%2C%22offset%22%3A0%2C%22pageSize%22%3A100%7D%5D%2C%22queryFormulas%22%3A%5B%5D%7D%2C%22clickhouse_sql%22%3A%5B%7B%22name%22%3A%22A%22%2C%22legend%22%3A%22%22%2C%22disabled%22%3Afalse%2C%22query%22%3A%22%22%7D%5D%2C%22promql%22%3A%5B%7B%22name%22%3A%22A%22%2C%22query%22%3A%22%22%2C%22legend%22%3A%22%22%2C%22disabled%22%3Afalse%7D%5D%2C%22queryType%22%3A%22builder%22%7D&relativeTime=30m&options=%7B%22selectColumns%22%3A%5B%5D%2C%22maxLines%22%3A2%2C%22format%22%3A%22list%22%2C%22fontSize%22%3A%22small%22%7D';
|
||||
|
||||
// Mocks
|
||||
|
||||
jest.mock('components/OverlayScrollbar/OverlayScrollbar', () => ({
|
||||
__esModule: true,
|
||||
default: ({ children }: { children: React.ReactNode }): JSX.Element => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: (): { pathname: string; search: string } => ({
|
||||
pathname: '',
|
||||
search: MOCK_SEARCH_PARAMS,
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): { safeNavigate: jest.Mock } => ({
|
||||
safeNavigate: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => <div>MockDateTimeSelection</div>,
|
||||
}));
|
||||
|
||||
// Helpers
|
||||
const getBuilderQuery = (payload: QueryRangePayload): IBuilderQuery =>
|
||||
payload.compositeQuery.builderQueries?.A as IBuilderQuery;
|
||||
|
||||
const assertTimeRangeConsistency = (
|
||||
payload: QueryRangePayload,
|
||||
initialTimeRange: { start: number; end: number },
|
||||
): void => {
|
||||
expect(payload.start).toBe(initialTimeRange.start);
|
||||
expect(payload.end).toBe(initialTimeRange.end);
|
||||
};
|
||||
|
||||
jest.setTimeout(20000);
|
||||
|
||||
Object.defineProperty(globalThis, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((query) => ({
|
||||
matches: true,
|
||||
media: query,
|
||||
addListener: (listener: (params: { matches: boolean }) => void): void => {
|
||||
listener({ matches: true });
|
||||
},
|
||||
removeListener: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
describe('LogsPanelComponent', () => {
|
||||
let capturedQueryRangePayloads: QueryRangePayload[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
capturedQueryRangePayloads = [];
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
capturedQueryRangePayloads.push(payload);
|
||||
|
||||
const queryData = getBuilderQuery(payload);
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(
|
||||
logsPaginationQueryRangeSuccessResponse({
|
||||
offset: queryData?.offset ?? 0,
|
||||
pageSize: 10,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const renderComponent = async (): Promise<void> => {
|
||||
render(
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<PreferenceContextProvider>
|
||||
<NewWidget
|
||||
dashboardId=""
|
||||
dashboardData={undefined}
|
||||
selectedGraph={PANEL_TYPES.LIST}
|
||||
/>
|
||||
</PreferenceContextProvider>
|
||||
</I18nextProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('No data')).not.toBeInTheDocument();
|
||||
});
|
||||
};
|
||||
|
||||
it.skip('should handle pagination flows correctly', async () => {
|
||||
await renderComponent();
|
||||
const initialTimeRange = {
|
||||
start: capturedQueryRangePayloads[0].start,
|
||||
end: capturedQueryRangePayloads[0].end,
|
||||
};
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /next/i }));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedQueryRangePayloads).toHaveLength(2);
|
||||
});
|
||||
|
||||
const firstPayload = capturedQueryRangePayloads[0];
|
||||
const secondPayload = capturedQueryRangePayloads[1];
|
||||
|
||||
const firstQueryData = getBuilderQuery(firstPayload);
|
||||
const secondQueryData = getBuilderQuery(secondPayload);
|
||||
|
||||
expect(firstQueryData.offset).toBe(0);
|
||||
expect(secondQueryData.offset).toBe(10);
|
||||
assertTimeRangeConsistency(secondPayload, initialTimeRange);
|
||||
const idFilter = secondQueryData.filters?.items?.find(
|
||||
(item) => item?.key?.key === 'id',
|
||||
);
|
||||
expect(idFilter).toBeUndefined();
|
||||
|
||||
const secondOrderByTimestamp = secondQueryData.orderBy?.find(
|
||||
(item) => item.columnName === 'timestamp',
|
||||
);
|
||||
const secondOrderById = secondQueryData.orderBy?.find(
|
||||
(item) => item.columnName === 'id',
|
||||
);
|
||||
expect(secondOrderByTimestamp).toBeDefined();
|
||||
expect(secondOrderById).toBeDefined();
|
||||
expect(secondOrderById?.order).toBe(secondOrderByTimestamp?.order);
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /previous/i }));
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedQueryRangePayloads).toHaveLength(3);
|
||||
});
|
||||
|
||||
const thirdPayload = capturedQueryRangePayloads[2];
|
||||
const thirdQueryData = getBuilderQuery(thirdPayload);
|
||||
expect(thirdQueryData.offset).toBe(0);
|
||||
assertTimeRangeConsistency(thirdPayload, initialTimeRange);
|
||||
const thirdIdFilter = thirdQueryData.filters?.items?.find(
|
||||
(item) => item?.key?.key === 'id',
|
||||
);
|
||||
expect(thirdIdFilter).toBeUndefined();
|
||||
|
||||
const thirdOrderByTimestamp = thirdQueryData.orderBy?.find(
|
||||
(item) => item.columnName === 'timestamp',
|
||||
);
|
||||
const thirdOrderById = thirdQueryData.orderBy?.find(
|
||||
(item) => item.columnName === 'id',
|
||||
);
|
||||
expect(thirdOrderByTimestamp).toBeDefined();
|
||||
expect(thirdOrderById).toBeDefined();
|
||||
expect(thirdOrderById?.order).toBe(thirdOrderByTimestamp?.order);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { TableColumnsType as ColumnsType } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { TimestampInput } from 'hooks/useTimezoneFormatter/useTimezoneFormatter';
|
||||
import { RowData } from 'lib/query/createTableColumnsFromQuery';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { IField } from 'types/api/logs/fields';
|
||||
|
||||
export const getLogPanelColumnsList = (
|
||||
|
||||
@@ -9,8 +9,8 @@ import setLocalStorageApi from 'api/browser/localstorage/set';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import GridCard from 'container/GridCardLayout/GridCard';
|
||||
import { Card, CardContainer } from 'container/GridCardLayout/styles';
|
||||
import GridCard from 'container/WidgetCard/Card';
|
||||
import { Card, CardContainer } from 'container/WidgetCard/styles';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import dayjs from 'dayjs';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
@@ -18,7 +18,7 @@ import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetWidgetQueryBuilderProps } from 'container/MetricsApplication/types';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderFormula,
|
||||
|
||||
@@ -14,6 +14,7 @@ import RightToolbarActions from 'container/QueryBuilder/components/ToolbarAction
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import DateTimeSelector from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { ExportDashboard } from 'hooks/dashboard/useExportDashboards';
|
||||
import { useGetExportToDashboardLink } from 'hooks/dashboard/useGetExportToDashboardLink';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
@@ -21,7 +22,6 @@ import { Filter } from '@signozhq/icons';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { MeterExplorerEventKeys, MeterExplorerEvents } from '../events';
|
||||
@@ -38,6 +38,7 @@ function Explorer(): JSX.Element {
|
||||
currentQuery,
|
||||
} = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const getExportToDashboardLink = useGetExportToDashboardLink();
|
||||
const queryClient = useQueryClient();
|
||||
const [isLoadingQueries, setIsLoadingQueries] = useState(false);
|
||||
const [isCancelled, setIsCancelled] = useState(false);
|
||||
@@ -91,16 +92,18 @@ function Explorer(): JSX.Element {
|
||||
|
||||
const widgetId = uuid();
|
||||
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
const dashboardEditView = getExportToDashboardLink({
|
||||
query: queryToExport || exportDefaultQuery,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
dashboardId: dashboard.id,
|
||||
widgetId,
|
||||
});
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
if (dashboardEditView) {
|
||||
safeNavigate(dashboardEditView);
|
||||
}
|
||||
},
|
||||
[exportDefaultQuery, safeNavigate],
|
||||
[exportDefaultQuery, safeNavigate, getExportToDashboardLink],
|
||||
);
|
||||
|
||||
const splitedQueries = useMemo(
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo, useRef } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getLegend } from 'lib/dashboard/getQueryResults';
|
||||
import getLabelName from 'lib/getLabelName';
|
||||
import {
|
||||
@@ -72,7 +71,7 @@ export function buildMeterChartConfig({
|
||||
show: true,
|
||||
side: 2,
|
||||
isDarkMode,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
isTimeAxis: true,
|
||||
});
|
||||
|
||||
builder.addAxis({
|
||||
@@ -81,7 +80,6 @@ export function buildMeterChartConfig({
|
||||
side: 3,
|
||||
isDarkMode,
|
||||
yAxisUnit,
|
||||
panelType: PANEL_TYPES.BAR,
|
||||
});
|
||||
|
||||
if (!apiResponse?.data?.result) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { GetWidgetQueryBuilderProps } from './types';
|
||||
|
||||
@@ -7,7 +7,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import Graph from 'container/WidgetCard/Card';
|
||||
import {
|
||||
databaseCallsAvgDuration,
|
||||
databaseCallsRPS,
|
||||
|
||||
@@ -7,7 +7,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import Graph from 'container/WidgetCard/Card';
|
||||
import {
|
||||
externalCallDuration,
|
||||
externalCallDurationByAddress,
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
} from 'constants/apDex';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import DisplayThreshold from 'container/GridCardLayout/WidgetHeader/DisplayThreshold';
|
||||
import Graph from 'container/WidgetCard/Card';
|
||||
import DisplayThreshold from 'container/WidgetCard/Header/DisplayThreshold';
|
||||
import {
|
||||
GraphTitle,
|
||||
SERVICE_CHART_ID,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import Graph from 'container/WidgetCard/Card';
|
||||
import {
|
||||
GraphTitle,
|
||||
SERVICE_CHART_ID,
|
||||
|
||||
@@ -2,11 +2,11 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import axios from 'axios';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { ENTITY_VERSION_V4 } from 'constants/app';
|
||||
import Graph from 'container/GridCardLayout/GridCard';
|
||||
import Graph from 'container/WidgetCard/Card';
|
||||
import { SERVICE_DETAIL_DRILLDOWN_ENABLED } from 'container/MetricsApplication/constant';
|
||||
import { Card, GraphContainer } from 'container/MetricsApplication/styles';
|
||||
import { OnClickPluginOpts } from 'lib/uPlotLib/plugins/onClickPlugin';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
|
||||
function TopLevelOperation({
|
||||
name,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { DownloadOptions } from 'container/Download/Download.types';
|
||||
import { MenuItemKeys } from 'container/GridCardLayout/WidgetHeader/contants';
|
||||
import { MenuItemKeys } from 'container/WidgetCard/Header/contants';
|
||||
import {
|
||||
MetricAggregateOperator,
|
||||
TracesAggregatorOperator,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Widgets } from 'types/api/widgets/widget';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { IServiceName } from './Tabs/types';
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import * as useOptionsMenuHooks from 'container/OptionsMenu';
|
||||
import * as useUpdateDashboardHooks from 'hooks/dashboard/useUpdateDashboard';
|
||||
import * as useQueryBuilderHooks from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import * as useHandleExplorerTabChangeHooks from 'hooks/useHandleExplorerTabChange';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
@@ -95,10 +94,6 @@ jest.mock('react-redux', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.spyOn(useUpdateDashboardHooks, 'useUpdateDashboard').mockReturnValue({
|
||||
mutate: jest.fn(),
|
||||
isLoading: false,
|
||||
} as any);
|
||||
jest.spyOn(useOptionsMenuHooks, 'useOptionsMenu').mockReturnValue({
|
||||
options: {
|
||||
selectColumns: [],
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Typography } from '@signozhq/ui/typography';
|
||||
import { Spin } from 'antd';
|
||||
import { useGetMetricReductionRuleTimeseries } from 'api/generated/services/metrics';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
||||
import { buildBaseConfig } from 'container/DashboardContainer/visualization/panels/utils/baseConfigBuilder';
|
||||
import BarChart from 'lib/visualization/charts/BarChart/BarChart';
|
||||
import { buildBaseConfig } from 'lib/visualization/panels/utils/baseConfigBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { Empty } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import Spinner from 'components/Spinner';
|
||||
import { EXCLUDED_COLUMNS } from 'container/OptionsMenu/constants';
|
||||
import { QueryKeySuggestionsResponseProps } from 'types/api/querySuggestions/types';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
type ExplorerAttributeColumnsProps = {
|
||||
isLoading: boolean;
|
||||
data: AxiosResponse<QueryKeySuggestionsResponseProps> | undefined;
|
||||
searchText: string;
|
||||
isAttributeKeySelected: (key: string) => boolean;
|
||||
handleCheckboxChange: (key: string) => void;
|
||||
dataSource: DataSource;
|
||||
};
|
||||
|
||||
function ExplorerAttributeColumns({
|
||||
isLoading,
|
||||
data,
|
||||
searchText,
|
||||
isAttributeKeySelected,
|
||||
handleCheckboxChange,
|
||||
dataSource,
|
||||
}: ExplorerAttributeColumnsProps): JSX.Element {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="attribute-columns">
|
||||
<Spinner size="large" tip="Loading..." height="2vh" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const filteredAttributeKeys =
|
||||
Object.values(data?.data?.data?.keys || {})
|
||||
?.flat()
|
||||
?.filter(
|
||||
(attributeKey) =>
|
||||
attributeKey.name.toLowerCase().includes(searchText.toLowerCase()) &&
|
||||
!EXCLUDED_COLUMNS[dataSource].includes(attributeKey.name),
|
||||
) || [];
|
||||
if (filteredAttributeKeys.length === 0) {
|
||||
return (
|
||||
<div className="attribute-columns">
|
||||
<Empty description="No columns found" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="attribute-columns">
|
||||
{filteredAttributeKeys.map((attributeKey: any) => (
|
||||
<Checkbox
|
||||
value={isAttributeKeySelected(attributeKey.name)}
|
||||
onChange={(): void => handleCheckboxChange(attributeKey.name)}
|
||||
key={attributeKey.name}
|
||||
>
|
||||
{attributeKey.name}
|
||||
</Checkbox>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExplorerAttributeColumns;
|
||||
@@ -1,136 +0,0 @@
|
||||
.explorer-columns-renderer {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding-left: 16px;
|
||||
}
|
||||
|
||||
.ant-typography {
|
||||
color: var(rgba(255, 255, 255, 0.85));
|
||||
font-family: 'Inter';
|
||||
font-size: 13px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
--divider-color: var(--l1-border);
|
||||
--divider-margin: 8px 0;
|
||||
}
|
||||
|
||||
.explorer-columns-contents {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-left: 16px;
|
||||
padding-right: 8px;
|
||||
|
||||
.explorer-columns {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
overflow-x: scroll;
|
||||
min-width: 90%;
|
||||
|
||||
.explorer-columns-list {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.explorer-column-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px;
|
||||
min-width: 200px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid
|
||||
var(
|
||||
--colorBorder,
|
||||
color-mix(in srgb, var(--bg-robin-300) 12%, transparent)
|
||||
);
|
||||
background: var(--l1-border);
|
||||
cursor: unset;
|
||||
|
||||
.explorer-column-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.lucide-trash2 {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.explorer-columns::-webkit-scrollbar {
|
||||
height: 0px; /* Height of the scrollbar */
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0px 16px;
|
||||
border-radius: 2px;
|
||||
background: var(--bg-robin-400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.explorer-columns-search {
|
||||
border: 1px solid color-mix(in srgb, var(--bg-robin-300) 12%, transparent);
|
||||
border-radius: 6px;
|
||||
padding: 0px;
|
||||
background: var(--l1-background);
|
||||
> input {
|
||||
height: 32px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.explorer-columns-dropdown {
|
||||
height: 200px;
|
||||
background-color: var(--l1-border);
|
||||
overflow: hidden !important;
|
||||
|
||||
padding: 4px;
|
||||
.ant-checkbox-wrapper {
|
||||
padding: 2px 8px !important;
|
||||
}
|
||||
|
||||
.attribute-columns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 160px;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.attribute-columns::-webkit-scrollbar {
|
||||
width: 3px; /* Width of the scrollbar */
|
||||
}
|
||||
|
||||
.attribute-columns::-webkit-scrollbar-track {
|
||||
background: var(--l1-border); /* Color of the track */
|
||||
}
|
||||
|
||||
.attribute-columns::-webkit-scrollbar-thumb {
|
||||
background: var(--l2-foreground); /* Color of the thumb */
|
||||
border-radius: 4px; /* Roundness of the thumb */
|
||||
}
|
||||
|
||||
.attribute-columns::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--l1-border); /* Color of the thumb on hover */
|
||||
}
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
DragDropContext,
|
||||
Draggable,
|
||||
Droppable,
|
||||
DropResult,
|
||||
} from 'react-beautiful-dnd';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@signozhq/ui/dropdown-menu';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { FieldDataType } from 'api/v5/v5';
|
||||
import { SOMETHING_WENT_WRONG } from 'constants/api';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import {
|
||||
CircleAlert,
|
||||
CirclePlus,
|
||||
GripVertical,
|
||||
Search,
|
||||
Trash2,
|
||||
} from '@signozhq/icons';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { WidgetGraphProps } from '../types';
|
||||
import ExplorerAttributeColumns from './ExplorerAttributeColumns';
|
||||
|
||||
import './ExplorerColumnsRenderer.styles.scss';
|
||||
|
||||
type LogColumnsRendererProps = {
|
||||
setSelectedLogFields: WidgetGraphProps['setSelectedLogFields'];
|
||||
selectedLogFields: WidgetGraphProps['selectedLogFields'];
|
||||
selectedTracesFields: WidgetGraphProps['selectedTracesFields'];
|
||||
setSelectedTracesFields: WidgetGraphProps['setSelectedTracesFields'];
|
||||
};
|
||||
|
||||
function ExplorerColumnsRenderer({
|
||||
selectedLogFields,
|
||||
setSelectedLogFields,
|
||||
selectedTracesFields,
|
||||
setSelectedTracesFields,
|
||||
}: LogColumnsRendererProps): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [querySearchText, setQuerySearchText] = useState<string>('');
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const initialDataSource = currentQuery.builder.queryData[0].dataSource;
|
||||
|
||||
// const { data, isLoading, isError } = useGetAggregateKeys(
|
||||
// {
|
||||
// aggregateAttribute: '',
|
||||
// dataSource: currentQuery.builder.queryData[0].dataSource,
|
||||
// aggregateOperator: currentQuery.builder.queryData[0].aggregateOperator,
|
||||
// searchText: querySearchText,
|
||||
// tagType: '',
|
||||
// },
|
||||
// {
|
||||
// queryKey: [
|
||||
// currentQuery.builder.queryData[0].dataSource,
|
||||
// currentQuery.builder.queryData[0].aggregateOperator,
|
||||
// querySearchText,
|
||||
// ],
|
||||
// },
|
||||
// );
|
||||
|
||||
const { data, isLoading, isError } = useGetQueryKeySuggestions(
|
||||
{
|
||||
searchText: querySearchText,
|
||||
signal: currentQuery.builder.queryData[0].dataSource,
|
||||
},
|
||||
{
|
||||
queryKey: [
|
||||
currentQuery.builder.queryData[0].dataSource,
|
||||
currentQuery.builder.queryData[0].aggregateOperator,
|
||||
querySearchText,
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const isAttributeKeySelected = (key: string): boolean => {
|
||||
if (initialDataSource === DataSource.LOGS && selectedLogFields) {
|
||||
return selectedLogFields.some((field) => field.name === key);
|
||||
}
|
||||
if (initialDataSource === DataSource.TRACES && selectedTracesFields) {
|
||||
return selectedTracesFields.some((field) => field.name === key);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleCheckboxChange = (key: string): void => {
|
||||
if (
|
||||
initialDataSource === DataSource.LOGS &&
|
||||
setSelectedLogFields !== undefined
|
||||
) {
|
||||
if (selectedLogFields) {
|
||||
if (isAttributeKeySelected(key)) {
|
||||
setSelectedLogFields(
|
||||
selectedLogFields.filter((field) => field.name !== key),
|
||||
);
|
||||
} else {
|
||||
setSelectedLogFields([
|
||||
...selectedLogFields,
|
||||
{ dataType: 'string', name: key, type: '' },
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
setSelectedLogFields([{ dataType: 'string', name: key, type: '' }]);
|
||||
}
|
||||
} else if (
|
||||
initialDataSource === DataSource.TRACES &&
|
||||
setSelectedTracesFields !== undefined
|
||||
) {
|
||||
const selectedField = Object.values(data?.data?.data?.keys || {})
|
||||
?.flat()
|
||||
?.find((attributeKey) => attributeKey.name === key);
|
||||
|
||||
if (selectedTracesFields) {
|
||||
if (isAttributeKeySelected(key)) {
|
||||
setSelectedTracesFields(
|
||||
selectedTracesFields.filter((field) => field.name !== key),
|
||||
);
|
||||
} else if (selectedField) {
|
||||
setSelectedTracesFields([
|
||||
...selectedTracesFields,
|
||||
{
|
||||
...selectedField,
|
||||
fieldDataType: selectedField.fieldDataType as FieldDataType,
|
||||
},
|
||||
]);
|
||||
}
|
||||
} else if (selectedField) {
|
||||
setSelectedTracesFields([
|
||||
{
|
||||
...selectedField,
|
||||
fieldDataType: selectedField.fieldDataType as FieldDataType,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const debouncedSetQuerySearchText = useDebouncedFn((value) => {
|
||||
setQuerySearchText(value as string);
|
||||
}, 400);
|
||||
|
||||
useEffect(
|
||||
() => (): void => {
|
||||
debouncedSetQuerySearchText.cancel();
|
||||
},
|
||||
[debouncedSetQuerySearchText],
|
||||
);
|
||||
|
||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
setSearchText(e.target.value);
|
||||
debouncedSetQuerySearchText(e.target.value);
|
||||
};
|
||||
|
||||
const handleOpenChange = (nextOpen: boolean): void => {
|
||||
setOpen(nextOpen);
|
||||
if (nextOpen) {
|
||||
setSearchText('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeSelectedLogField = (name: string): void => {
|
||||
if (
|
||||
initialDataSource === DataSource.LOGS &&
|
||||
setSelectedLogFields &&
|
||||
selectedLogFields
|
||||
) {
|
||||
setSelectedLogFields(
|
||||
selectedLogFields.filter((field) => field.name !== name),
|
||||
);
|
||||
}
|
||||
if (
|
||||
initialDataSource === DataSource.TRACES &&
|
||||
setSelectedTracesFields &&
|
||||
selectedTracesFields
|
||||
) {
|
||||
setSelectedTracesFields(
|
||||
selectedTracesFields.filter((field) => field.name !== name),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const onDragEnd = (result: DropResult): void => {
|
||||
if (!result.destination) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
initialDataSource === DataSource.LOGS &&
|
||||
selectedLogFields &&
|
||||
setSelectedLogFields
|
||||
) {
|
||||
const items = [...selectedLogFields];
|
||||
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||
items.splice(result.destination.index, 0, reorderedItem);
|
||||
|
||||
setSelectedLogFields(items);
|
||||
}
|
||||
if (
|
||||
initialDataSource === DataSource.TRACES &&
|
||||
selectedTracesFields &&
|
||||
setSelectedTracesFields
|
||||
) {
|
||||
const items = [...selectedTracesFields];
|
||||
const [reorderedItem] = items.splice(result.source.index, 1);
|
||||
items.splice(result.destination.index, 0, reorderedItem);
|
||||
|
||||
setSelectedTracesFields(items);
|
||||
}
|
||||
};
|
||||
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
return (
|
||||
<div className="explorer-columns-renderer">
|
||||
<div className="title">
|
||||
<Typography.Text>Columns</Typography.Text>
|
||||
{isError && (
|
||||
<Tooltip title={SOMETHING_WENT_WRONG}>
|
||||
<CircleAlert size={16} data-testid="alert-circle-icon" />
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
<Divider className="explorer-columns-renderer__divider" />
|
||||
{!isError && (
|
||||
<div className="explorer-columns-contents">
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="drag-drop-list" direction="horizontal">
|
||||
{(provided): JSX.Element => (
|
||||
<div
|
||||
className="explorer-columns"
|
||||
{...provided.droppableProps}
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{initialDataSource === DataSource.LOGS &&
|
||||
selectedLogFields &&
|
||||
selectedLogFields.map((field, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Draggable key={index} draggableId={index.toString()} index={index}>
|
||||
{(dragProvided): JSX.Element => (
|
||||
<div
|
||||
className="explorer-column-card"
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
>
|
||||
<div className="explorer-column-title">
|
||||
<GripVertical size={12} color="#5A5A5A" />
|
||||
{field.name}
|
||||
</div>
|
||||
<Trash2
|
||||
size={12}
|
||||
color="red"
|
||||
onClick={(): void => removeSelectedLogField(field.name)}
|
||||
data-testid="trash-icon"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{initialDataSource === DataSource.TRACES &&
|
||||
selectedTracesFields &&
|
||||
selectedTracesFields.map((field, index) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<Draggable key={index} draggableId={index.toString()} index={index}>
|
||||
{(dragProvided): JSX.Element => (
|
||||
<div
|
||||
className="explorer-column-card"
|
||||
ref={dragProvided.innerRef}
|
||||
{...dragProvided.draggableProps}
|
||||
{...dragProvided.dragHandleProps}
|
||||
>
|
||||
<div className="explorer-column-title">
|
||||
<GripVertical size={12} color="#5A5A5A" />
|
||||
{field?.name || (field as any)?.key}
|
||||
</div>
|
||||
<Trash2
|
||||
size={12}
|
||||
color="red"
|
||||
onClick={(): void =>
|
||||
removeSelectedLogField(field?.name || (field as any)?.key)
|
||||
}
|
||||
data-testid="trash-icon"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<div>
|
||||
<DropdownMenu open={open} onOpenChange={handleOpenChange}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="action-btn"
|
||||
data-testid="add-columns-button"
|
||||
icon={
|
||||
<CirclePlus
|
||||
size={16}
|
||||
color={isDarkMode ? Color.BG_INK_400 : Color.BG_VANILLA_100}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" className="explorer-columns-dropdown">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
className="explorer-columns-search"
|
||||
value={searchText}
|
||||
onChange={handleSearchChange}
|
||||
prefix={<Search size={16} style={{ padding: '6px' }} />}
|
||||
/>
|
||||
<ExplorerAttributeColumns
|
||||
isLoading={isLoading}
|
||||
data={data}
|
||||
searchText={searchText}
|
||||
isAttributeKeySelected={isAttributeKeySelected}
|
||||
handleCheckboxChange={handleCheckboxChange}
|
||||
dataSource={initialDataSource}
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ExplorerColumnsRenderer;
|
||||
@@ -1,9 +0,0 @@
|
||||
.query-section-left-container {
|
||||
border: none;
|
||||
border-top: 1px solid var(--l1-border);
|
||||
background: var(--l1-background);
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user