mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-02 02:20:20 +01:00
Compare commits
47 Commits
refactor/c
...
fix/hosts-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1dd15e165 | ||
|
|
bbd5113cec | ||
|
|
75610e21c5 | ||
|
|
cb6325a8da | ||
|
|
2af86f1f84 | ||
|
|
a237c2c15d | ||
|
|
5dff8601ba | ||
|
|
112406452a | ||
|
|
b9008cd618 | ||
|
|
1f43feaf3c | ||
|
|
37d202de92 | ||
|
|
87e5ef2f0a | ||
|
|
b151bcd697 | ||
|
|
bb4e7df68b | ||
|
|
198b54252d | ||
|
|
e4f0d026c1 | ||
|
|
754dbc7f38 | ||
|
|
07dbf1e69f | ||
|
|
abe5454d11 | ||
|
|
749884ce70 | ||
|
|
70fdc88112 | ||
|
|
234787f2c1 | ||
|
|
e7d0dd8850 | ||
|
|
c23c6c8c27 | ||
|
|
58dabf9a6c | ||
|
|
91edc2a895 | ||
|
|
2e70857554 | ||
|
|
f3e6892d5b | ||
|
|
23a4960e74 | ||
|
|
5d0c55d682 | ||
|
|
15704e0433 | ||
|
|
5db0501c02 | ||
|
|
73da474563 | ||
|
|
028c134ea9 | ||
|
|
31b61a89fd | ||
|
|
8609f43fe0 | ||
|
|
658f794842 | ||
|
|
e9abd5ddfc | ||
|
|
ea2663b145 | ||
|
|
234716df53 | ||
|
|
531979543c | ||
|
|
4b09f057b9 | ||
|
|
dde7c79b4d | ||
|
|
c95523c747 | ||
|
|
63cb54c5b0 | ||
|
|
19e8196472 | ||
|
|
c360e4498d |
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
@@ -86,6 +86,8 @@ go.mod @therealpandey
|
||||
/pkg/types/alertmanagertypes @srikanthccv
|
||||
/pkg/alertmanager/ @srikanthccv
|
||||
/pkg/ruler/ @srikanthccv
|
||||
/pkg/modules/rulestatehistory/ @srikanthccv
|
||||
/pkg/types/rulestatehistorytypes/ @srikanthccv
|
||||
|
||||
# Correlation-adjacent
|
||||
|
||||
@@ -105,7 +107,7 @@ go.mod @therealpandey
|
||||
/pkg/modules/authdomain/ @vikrantgupta25
|
||||
/pkg/modules/role/ @vikrantgupta25
|
||||
|
||||
# IdentN Owners
|
||||
# IdentN Owners
|
||||
/pkg/identn/ @vikrantgupta25
|
||||
/pkg/http/middleware/identn.go @vikrantgupta25
|
||||
|
||||
|
||||
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -17,5 +17,7 @@
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features"
|
||||
}
|
||||
},
|
||||
"python-envs.defaultEnvManager": "ms-python.python:system",
|
||||
"python-envs.pythonProjects": []
|
||||
}
|
||||
|
||||
@@ -144,6 +144,8 @@ telemetrystore:
|
||||
|
||||
##################### Prometheus #####################
|
||||
prometheus:
|
||||
# The maximum time a PromQL query is allowed to run before being aborted.
|
||||
timeout: 2m
|
||||
active_query_tracker:
|
||||
# Whether to enable the active query tracker.
|
||||
enabled: true
|
||||
|
||||
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.116.1
|
||||
image: signoz/signoz:v0.117.1
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.116.1
|
||||
image: signoz/signoz:v0.117.1
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.116.1}
|
||||
image: signoz/signoz:${VERSION:-v0.117.1}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.116.1}
|
||||
image: signoz/signoz:${VERSION:-v0.117.1}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
2319
docs/api/openapi.yml
2319
docs/api/openapi.yml
File diff suppressed because it is too large
Load Diff
@@ -123,6 +123,7 @@ if err := router.Handle("/api/v1/things", handler.New(
|
||||
Description: "This endpoint creates a thing",
|
||||
Request: new(types.PostableThing),
|
||||
RequestContentType: "application/json",
|
||||
RequestQuery: new(types.QueryableThing),
|
||||
Response: new(types.GettableThing),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
@@ -155,6 +156,8 @@ The `handler.New` function ties the HTTP handler to OpenAPI metadata via `OpenAP
|
||||
- **Request / RequestContentType**:
|
||||
- `Request` is a Go type that describes the request body or form.
|
||||
- `RequestContentType` is usually `"application/json"` or `"application/x-www-form-urlencoded"` (for callbacks like SAML).
|
||||
- **RequestQuery**:
|
||||
- `RequestQuery` is a Go type that descirbes query url params.
|
||||
- **RequestExamples**: An array of `handler.OpenAPIExample` that provide concrete request payloads in the generated spec. See [Adding request examples](#adding-request-examples) below.
|
||||
- **Response / ResponseContentType**:
|
||||
- `Response` is the Go type for the successful response payload.
|
||||
|
||||
@@ -273,6 +273,7 @@ Options can be simple (direct link) or nested (with another question):
|
||||
- Place logo files in `public/Logos/`
|
||||
- Use SVG format
|
||||
- Reference as `"/Logos/your-logo.svg"`
|
||||
- **Fetching Icons**: New icons can be easily fetched from [OpenBrand](https://openbrand.sh/). Use the pattern `https://openbrand.sh/?url=<TARGET_URL>`, where `<TARGET_URL>` is the URL-encoded link to the service's website. For example, to get Render's logo, use [https://openbrand.sh/?url=https%3A%2F%2Frender.com](https://openbrand.sh/?url=https%3A%2F%2Frender.com).
|
||||
- **Optimize new SVGs**: Run any newly downloaded SVGs through an optimizer like [SVGOMG (svgo)](https://svgomg.net/) or use `npx svgo public/Logos/your-logo.svg` to minimise their size before committing.
|
||||
|
||||
### 4. Links
|
||||
|
||||
@@ -57,6 +57,10 @@ func (provider *provider) Start(ctx context.Context) error {
|
||||
return provider.openfgaServer.Start(ctx)
|
||||
}
|
||||
|
||||
func (provider *provider) Healthy() <-chan struct{} {
|
||||
return provider.openfgaServer.Healthy()
|
||||
}
|
||||
|
||||
func (provider *provider) Stop(ctx context.Context) error {
|
||||
return provider.openfgaServer.Stop(ctx)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ type Server struct {
|
||||
}
|
||||
|
||||
func NewOpenfgaServer(ctx context.Context, pkgAuthzService authz.AuthZ) (*Server, error) {
|
||||
|
||||
return &Server{
|
||||
pkgAuthzService: pkgAuthzService,
|
||||
}, nil
|
||||
@@ -26,6 +25,10 @@ func (server *Server) Start(ctx context.Context) error {
|
||||
return server.pkgAuthzService.Start(ctx)
|
||||
}
|
||||
|
||||
func (server *Server) Healthy() <-chan struct{} {
|
||||
return server.pkgAuthzService.Healthy()
|
||||
}
|
||||
|
||||
func (server *Server) Stop(ctx context.Context) error {
|
||||
return server.pkgAuthzService.Stop(ctx)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/cache"
|
||||
"github.com/SigNoz/signoz/pkg/http/middleware"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/prometheus"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
"github.com/SigNoz/signoz/pkg/signoz"
|
||||
@@ -106,6 +107,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
signoz.TelemetryMetadataStore,
|
||||
signoz.Prometheus,
|
||||
signoz.Modules.OrgGetter,
|
||||
signoz.Modules.RuleStateHistory,
|
||||
signoz.Querier,
|
||||
signoz.Instrumentation.ToProviderSettings(),
|
||||
signoz.QueryParser,
|
||||
@@ -136,6 +138,7 @@ func NewServer(config signoz.Config, signoz *signoz.SigNoz) (*Server, error) {
|
||||
logParsingPipelineController, err := logparsingpipeline.NewLogParsingPipelinesController(
|
||||
signoz.SQLStore,
|
||||
integrationsController.GetPipelinesForInstalledIntegrations,
|
||||
reader,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -343,28 +346,29 @@ func (s *Server) Stop(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
|
||||
func makeRulesManager(ch baseint.Reader, cache cache.Cache, alertmanager alertmanager.Alertmanager, sqlstore sqlstore.SQLStore, telemetryStore telemetrystore.TelemetryStore, metadataStore telemetrytypes.MetadataStore, prometheus prometheus.Prometheus, orgGetter organization.Getter, ruleStateHistoryModule rulestatehistory.Module, querier querier.Querier, providerSettings factory.ProviderSettings, queryParser queryparser.QueryParser) (*baserules.Manager, error) {
|
||||
ruleStore := sqlrulestore.NewRuleStore(sqlstore, queryParser, providerSettings)
|
||||
maintenanceStore := sqlrulestore.NewMaintenanceStore(sqlstore)
|
||||
// create manager opts
|
||||
managerOpts := &baserules.ManagerOptions{
|
||||
TelemetryStore: telemetryStore,
|
||||
MetadataStore: metadataStore,
|
||||
Prometheus: prometheus,
|
||||
Context: context.Background(),
|
||||
Reader: ch,
|
||||
Querier: querier,
|
||||
Logger: providerSettings.Logger,
|
||||
Cache: cache,
|
||||
EvalDelay: baseconst.GetEvalDelay(),
|
||||
PrepareTaskFunc: rules.PrepareTaskFunc,
|
||||
PrepareTestRuleFunc: rules.TestNotification,
|
||||
Alertmanager: alertmanager,
|
||||
OrgGetter: orgGetter,
|
||||
RuleStore: ruleStore,
|
||||
MaintenanceStore: maintenanceStore,
|
||||
SqlStore: sqlstore,
|
||||
QueryParser: queryParser,
|
||||
TelemetryStore: telemetryStore,
|
||||
MetadataStore: metadataStore,
|
||||
Prometheus: prometheus,
|
||||
Context: context.Background(),
|
||||
Reader: ch,
|
||||
Querier: querier,
|
||||
Logger: providerSettings.Logger,
|
||||
Cache: cache,
|
||||
EvalDelay: baseconst.GetEvalDelay(),
|
||||
PrepareTaskFunc: rules.PrepareTaskFunc,
|
||||
PrepareTestRuleFunc: rules.TestNotification,
|
||||
Alertmanager: alertmanager,
|
||||
OrgGetter: orgGetter,
|
||||
RuleStore: ruleStore,
|
||||
MaintenanceStore: maintenanceStore,
|
||||
SqlStore: sqlstore,
|
||||
QueryParser: queryParser,
|
||||
RuleStateHistoryModule: ruleStateHistoryModule,
|
||||
}
|
||||
|
||||
// create Manager
|
||||
|
||||
@@ -28,6 +28,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
if err != nil {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "evaluation is invalid: %v", err)
|
||||
}
|
||||
|
||||
if opts.Rule.RuleType == ruletypes.RuleTypeThreshold {
|
||||
// create a threshold rule
|
||||
tr, err := baserules.NewThresholdRule(
|
||||
@@ -41,6 +42,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
|
||||
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -65,6 +67,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
|
||||
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
@@ -90,6 +93,7 @@ func PrepareTaskFunc(opts baserules.PrepareTaskOptions) (baserules.Task, error)
|
||||
baserules.WithSQLStore(opts.SQLStore),
|
||||
baserules.WithQueryParser(opts.ManagerOpts.QueryParser),
|
||||
baserules.WithMetadataStore(opts.ManagerOpts.MetadataStore),
|
||||
baserules.WithRuleStateHistoryModule(opts.ManagerOpts.RuleStateHistoryModule),
|
||||
)
|
||||
if err != nil {
|
||||
return task, err
|
||||
|
||||
@@ -257,7 +257,7 @@ func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
|
||||
WillReturnRows(samplesRows)
|
||||
|
||||
// Create Prometheus provider for this test
|
||||
promProvider = prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{}, store)
|
||||
promProvider = prometheustest.New(context.Background(), instrumentationtest.New().ToProviderSettings(), prometheus.Config{Timeout: 2 * time.Minute}, store)
|
||||
},
|
||||
ManagerOptionsHook: func(opts *rules.ManagerOptions) {
|
||||
// Set Prometheus provider for PromQL queries
|
||||
|
||||
@@ -205,6 +205,25 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['src/**/*.{jsx,tsx,ts}'],
|
||||
excludedFiles: [
|
||||
'**/*.test.{js,jsx,ts,tsx}',
|
||||
'**/*.spec.{js,jsx,ts,tsx}',
|
||||
'**/__tests__/**/*.{js,jsx,ts,tsx}',
|
||||
],
|
||||
rules: {
|
||||
'no-restricted-properties': [
|
||||
'error',
|
||||
{
|
||||
object: 'navigator',
|
||||
property: 'clipboard',
|
||||
message:
|
||||
'Do not use navigator.clipboard directly since it does not work well with specific browsers. Use hook useCopyToClipboard from react-use library. https://streamich.github.io/react-use/?path=/story/side-effects-usecopytoclipboard--docs',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
'**/*.test.{js,jsx,ts,tsx}',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
interface SafeNavigateOptions {
|
||||
replace?: boolean;
|
||||
state?: unknown;
|
||||
newTab?: boolean;
|
||||
}
|
||||
|
||||
interface SafeNavigateTo {
|
||||
@@ -20,9 +21,7 @@ interface UseSafeNavigateReturn {
|
||||
|
||||
export const useSafeNavigate = (): UseSafeNavigateReturn => ({
|
||||
safeNavigate: jest.fn(
|
||||
(to: SafeNavigateToType, options?: SafeNavigateOptions) => {
|
||||
console.log(`Mock safeNavigate called with:`, to, options);
|
||||
},
|
||||
(_to: SafeNavigateToType, _options?: SafeNavigateOptions) => {},
|
||||
) as jest.MockedFunction<
|
||||
(to: SafeNavigateToType, options?: SafeNavigateOptions) => void
|
||||
>,
|
||||
|
||||
@@ -164,6 +164,7 @@
|
||||
"vite-plugin-html": "3.2.2",
|
||||
"web-vitals": "^0.2.4",
|
||||
"xstate": "^4.31.0",
|
||||
"zod": "4.3.6",
|
||||
"zustand": "5.0.11"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -286,4 +287,4 @@
|
||||
"tmp": "0.2.4",
|
||||
"vite": "npm:rolldown-vite@7.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
frontend/public/Logos/mistral.svg
Normal file
1
frontend/public/Logos/mistral.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#fa520f" viewBox="0 0 24 24"><title>Mistral AI</title><path d="M17.143 3.429v3.428h-3.429v3.429h-3.428V6.857H6.857V3.43H3.43v13.714H0v3.428h10.286v-3.428H6.857v-3.429h3.429v3.429h3.429v-3.429h3.428v3.429h-3.428v3.428H24v-3.428h-3.43V3.429z"/></svg>
|
||||
|
After Width: | Height: | Size: 294 B |
1
frontend/public/Logos/openclaw.svg
Normal file
1
frontend/public/Logos/openclaw.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 120 120"><defs><linearGradient id="a" x1="0%" x2="100%" y1="0%" y2="100%"><stop offset="0%" stop-color="#ff4d4d"/><stop offset="100%" stop-color="#991b1b"/></linearGradient></defs><path fill="url(#a)" d="M60 10c-30 0-45 25-45 45s15 40 30 45v10h10v-10s5 2 10 0v10h10v-10c15-5 30-25 30-45S90 10 60 10"/><path fill="url(#a)" d="M20 45C5 40 0 50 5 60s15 5 20-5c3-7 0-10-5-10"/><path fill="url(#a)" d="M100 45c15-5 20 5 15 15s-15 5-20-5c-3-7 0-10 5-10"/><path stroke="#ff4d4d" stroke-linecap="round" stroke-width="3" d="M45 15Q35 5 30 8M75 15Q85 5 90 8"/><circle cx="45" cy="35" r="6" fill="#050810"/><circle cx="75" cy="35" r="6" fill="#050810"/><circle cx="46" cy="34" r="2.5" fill="#00e5cc"/><circle cx="76" cy="34" r="2.5" fill="#00e5cc"/></svg>
|
||||
|
After Width: | Height: | Size: 809 B |
1
frontend/public/Logos/render.svg
Normal file
1
frontend/public/Logos/render.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Render</title><path d="M18.263.007c-3.121-.147-5.744 2.109-6.192 5.082-.018.138-.045.272-.067.405-.696 3.703-3.936 6.507-7.827 6.507a7.9 7.9 0 0 1-3.825-.979.202.202 0 0 0-.302.178V24H12v-8.999c0-1.656 1.338-3 2.987-3h2.988c3.382 0 6.103-2.817 5.97-6.244-.12-3.084-2.61-5.603-5.682-5.75"/></svg>
|
||||
|
After Width: | Height: | Size: 362 B |
97
frontend/src/__tests__/query_range_v5.utility.ts
Normal file
97
frontend/src/__tests__/query_range_v5.utility.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { rest, RestRequest } from 'msw';
|
||||
|
||||
import { ENVIRONMENT } from '../constants/env';
|
||||
import { server } from '../mocks-server/server';
|
||||
import { MetricRangePayloadV5 } from '../types/api/v5/queryRange';
|
||||
|
||||
const QUERY_RANGE_URL = `${ENVIRONMENT.baseURL}/api/v5/query_range`;
|
||||
|
||||
export type MockLogsOptions = {
|
||||
offset?: number;
|
||||
pageSize?: number;
|
||||
hasMore?: boolean;
|
||||
delay?: number;
|
||||
onReceiveRequest?: (
|
||||
req: RestRequest,
|
||||
) => undefined | void | Omit<MockLogsOptions, 'onReceiveRequest'>;
|
||||
};
|
||||
|
||||
const createLogsResponse = ({
|
||||
offset = 0,
|
||||
pageSize = 100,
|
||||
hasMore = true,
|
||||
}: MockLogsOptions): MetricRangePayloadV5 => {
|
||||
const itemsForThisPage = hasMore ? pageSize : pageSize / 2;
|
||||
|
||||
return {
|
||||
data: {
|
||||
type: 'raw',
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
queryName: 'A',
|
||||
rows: Array.from({ length: itemsForThisPage }, (_, index) => {
|
||||
const cumulativeIndex = offset + index;
|
||||
return {
|
||||
timestamp: new Date(Date.now() - cumulativeIndex * 1000).toISOString(),
|
||||
data: {
|
||||
body: `Log message ${cumulativeIndex}`,
|
||||
id: `log-${cumulativeIndex}`,
|
||||
severity_text: 'INFO',
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
meta: {
|
||||
bytesScanned: 0,
|
||||
durationMs: 0,
|
||||
rowsScanned: 0,
|
||||
stepIntervals: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function mockQueryRangeV5WithLogsResponse({
|
||||
hasMore = true,
|
||||
offset = 0,
|
||||
pageSize = 100,
|
||||
delay = 0,
|
||||
onReceiveRequest,
|
||||
}: MockLogsOptions = {}): void {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (req, res, ctx) =>
|
||||
res(
|
||||
...(delay ? [ctx.delay(delay)] : []),
|
||||
ctx.status(200),
|
||||
ctx.json(
|
||||
createLogsResponse(
|
||||
onReceiveRequest?.(req) ?? {
|
||||
hasMore,
|
||||
pageSize,
|
||||
offset,
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function mockQueryRangeV5WithError(
|
||||
error: string,
|
||||
statusCode = 500,
|
||||
): void {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(statusCode),
|
||||
ctx.json({
|
||||
error,
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { CreatePublicDashboardProps } from 'types/api/dashboard/public/create';
|
||||
|
||||
@@ -8,7 +9,7 @@ const createPublicDashboard = async (
|
||||
props: CreatePublicDashboardProps,
|
||||
): Promise<SuccessResponseV2<CreatePublicDashboardProps>> => {
|
||||
|
||||
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = '30m' } = props;
|
||||
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props;
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import axios from 'api';
|
||||
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
|
||||
import { AxiosError } from 'axios';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
|
||||
import { UpdatePublicDashboardProps } from 'types/api/dashboard/public/update';
|
||||
|
||||
@@ -8,7 +9,7 @@ const updatePublicDashboard = async (
|
||||
props: UpdatePublicDashboardProps,
|
||||
): Promise<SuccessResponseV2<UpdatePublicDashboardProps>> => {
|
||||
|
||||
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = '30m' } = props;
|
||||
const { dashboardId, timeRangeEnabled = false, defaultTimeRange = DEFAULT_TIME_RANGE } = props;
|
||||
|
||||
try {
|
||||
const response = await axios.put(
|
||||
|
||||
1041
frontend/src/api/generated/services/cloudintegration/index.ts
Normal file
1041
frontend/src/api/generated/services/cloudintegration/index.ts
Normal file
File diff suppressed because it is too large
Load Diff
250
frontend/src/api/generated/services/health/index.ts
Normal file
250
frontend/src/api/generated/services/health/index.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'yarn generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import type { ErrorType } from '../../../generatedAPIInstance';
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type {
|
||||
Healthz200,
|
||||
Healthz503,
|
||||
Livez200,
|
||||
Readyz200,
|
||||
Readyz503,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
/**
|
||||
* @summary Health check
|
||||
*/
|
||||
export const healthz = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<Healthz200>({
|
||||
url: `/api/v2/healthz`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getHealthzQueryKey = () => {
|
||||
return [`/api/v2/healthz`] as const;
|
||||
};
|
||||
|
||||
export const getHealthzQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof healthz>>,
|
||||
TError = ErrorType<Healthz503>
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof healthz>>, TError, TData>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getHealthzQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof healthz>>> = ({
|
||||
signal,
|
||||
}) => healthz(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof healthz>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type HealthzQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof healthz>>
|
||||
>;
|
||||
export type HealthzQueryError = ErrorType<Healthz503>;
|
||||
|
||||
/**
|
||||
* @summary Health check
|
||||
*/
|
||||
|
||||
export function useHealthz<
|
||||
TData = Awaited<ReturnType<typeof healthz>>,
|
||||
TError = ErrorType<Healthz503>
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof healthz>>, TError, TData>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getHealthzQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Health check
|
||||
*/
|
||||
export const invalidateHealthz = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getHealthzQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Liveness check
|
||||
*/
|
||||
export const livez = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<Livez200>({
|
||||
url: `/api/v2/livez`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getLivezQueryKey = () => {
|
||||
return [`/api/v2/livez`] as const;
|
||||
};
|
||||
|
||||
export const getLivezQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof livez>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof livez>>, TError, TData>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getLivezQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof livez>>> = ({
|
||||
signal,
|
||||
}) => livez(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof livez>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type LivezQueryResult = NonNullable<Awaited<ReturnType<typeof livez>>>;
|
||||
export type LivezQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Liveness check
|
||||
*/
|
||||
|
||||
export function useLivez<
|
||||
TData = Awaited<ReturnType<typeof livez>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof livez>>, TError, TData>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getLivezQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Liveness check
|
||||
*/
|
||||
export const invalidateLivez = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries({ queryKey: getLivezQueryKey() }, options);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Readiness check
|
||||
*/
|
||||
export const readyz = (signal?: AbortSignal) => {
|
||||
return GeneratedAPIInstance<Readyz200>({
|
||||
url: `/api/v2/readyz`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getReadyzQueryKey = () => {
|
||||
return [`/api/v2/readyz`] as const;
|
||||
};
|
||||
|
||||
export const getReadyzQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof readyz>>,
|
||||
TError = ErrorType<Readyz503>
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof readyz>>, TError, TData>;
|
||||
}) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey = queryOptions?.queryKey ?? getReadyzQueryKey();
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof readyz>>> = ({
|
||||
signal,
|
||||
}) => readyz(signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof readyz>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ReadyzQueryResult = NonNullable<Awaited<ReturnType<typeof readyz>>>;
|
||||
export type ReadyzQueryError = ErrorType<Readyz503>;
|
||||
|
||||
/**
|
||||
* @summary Readiness check
|
||||
*/
|
||||
|
||||
export function useReadyz<
|
||||
TData = Awaited<ReturnType<typeof readyz>>,
|
||||
TError = ErrorType<Readyz503>
|
||||
>(options?: {
|
||||
query?: UseQueryOptions<Awaited<ReturnType<typeof readyz>>, TError, TData>;
|
||||
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getReadyzQueryOptions(options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Readiness check
|
||||
*/
|
||||
export const invalidateReadyz = async (
|
||||
queryClient: QueryClient,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getReadyzQueryKey() },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
@@ -20,11 +20,113 @@ import { useMutation, useQuery } from 'react-query';
|
||||
import type { BodyType, ErrorType } from '../../../generatedAPIInstance';
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type {
|
||||
HandleExportRawDataPOSTParams,
|
||||
ListPromotedAndIndexedPaths200,
|
||||
PromotetypesPromotePathDTO,
|
||||
Querybuildertypesv5QueryRangeRequestDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
/**
|
||||
* This endpoints allows complex query exporting raw data for traces and logs
|
||||
* @summary Export raw data
|
||||
*/
|
||||
export const handleExportRawDataPOST = (
|
||||
querybuildertypesv5QueryRangeRequestDTO: BodyType<Querybuildertypesv5QueryRangeRequestDTO>,
|
||||
params?: HandleExportRawDataPOSTParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<string>({
|
||||
url: `/api/v1/export_raw_data`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: querybuildertypesv5QueryRangeRequestDTO,
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getHandleExportRawDataPOSTMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof handleExportRawDataPOST>>,
|
||||
TError,
|
||||
{
|
||||
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
|
||||
params?: HandleExportRawDataPOSTParams;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof handleExportRawDataPOST>>,
|
||||
TError,
|
||||
{
|
||||
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
|
||||
params?: HandleExportRawDataPOSTParams;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['handleExportRawDataPOST'];
|
||||
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 handleExportRawDataPOST>>,
|
||||
{
|
||||
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
|
||||
params?: HandleExportRawDataPOSTParams;
|
||||
}
|
||||
> = (props) => {
|
||||
const { data, params } = props ?? {};
|
||||
|
||||
return handleExportRawDataPOST(data, params);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type HandleExportRawDataPOSTMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof handleExportRawDataPOST>>
|
||||
>;
|
||||
export type HandleExportRawDataPOSTMutationBody = BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
|
||||
export type HandleExportRawDataPOSTMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Export raw data
|
||||
*/
|
||||
export const useHandleExportRawDataPOST = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof handleExportRawDataPOST>>,
|
||||
TError,
|
||||
{
|
||||
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
|
||||
params?: HandleExportRawDataPOSTParams;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof handleExportRawDataPOST>>,
|
||||
TError,
|
||||
{
|
||||
data: BodyType<Querybuildertypesv5QueryRangeRequestDTO>;
|
||||
params?: HandleExportRawDataPOSTParams;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getHandleExportRawDataPOSTMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* This endpoints promotes and indexes paths
|
||||
* @summary Promote and index paths
|
||||
|
||||
744
frontend/src/api/generated/services/rules/index.ts
Normal file
744
frontend/src/api/generated/services/rules/index.ts
Normal file
@@ -0,0 +1,744 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'yarn generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import type { ErrorType } from '../../../generatedAPIInstance';
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type {
|
||||
GetRuleHistoryFilterKeys200,
|
||||
GetRuleHistoryFilterKeysParams,
|
||||
GetRuleHistoryFilterKeysPathParameters,
|
||||
GetRuleHistoryFilterValues200,
|
||||
GetRuleHistoryFilterValuesParams,
|
||||
GetRuleHistoryFilterValuesPathParameters,
|
||||
GetRuleHistoryOverallStatus200,
|
||||
GetRuleHistoryOverallStatusParams,
|
||||
GetRuleHistoryOverallStatusPathParameters,
|
||||
GetRuleHistoryStats200,
|
||||
GetRuleHistoryStatsParams,
|
||||
GetRuleHistoryStatsPathParameters,
|
||||
GetRuleHistoryTimeline200,
|
||||
GetRuleHistoryTimelineParams,
|
||||
GetRuleHistoryTimelinePathParameters,
|
||||
GetRuleHistoryTopContributors200,
|
||||
GetRuleHistoryTopContributorsParams,
|
||||
GetRuleHistoryTopContributorsPathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
/**
|
||||
* Returns distinct label keys from rule history entries for the selected range.
|
||||
* @summary Get rule history filter keys
|
||||
*/
|
||||
export const getRuleHistoryFilterKeys = (
|
||||
{ id }: GetRuleHistoryFilterKeysPathParameters,
|
||||
params?: GetRuleHistoryFilterKeysParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetRuleHistoryFilterKeys200>({
|
||||
url: `/api/v2/rules/${id}/history/filter_keys`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetRuleHistoryFilterKeysQueryKey = (
|
||||
{ id }: GetRuleHistoryFilterKeysPathParameters,
|
||||
params?: GetRuleHistoryFilterKeysParams,
|
||||
) => {
|
||||
return [
|
||||
`/api/v2/rules/${id}/history/filter_keys`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getGetRuleHistoryFilterKeysQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleHistoryFilterKeysPathParameters,
|
||||
params?: GetRuleHistoryFilterKeysParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetRuleHistoryFilterKeysQueryKey({ id }, params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>
|
||||
> = ({ signal }) => getRuleHistoryFilterKeys({ id }, params, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetRuleHistoryFilterKeysQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>
|
||||
>;
|
||||
export type GetRuleHistoryFilterKeysQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get rule history filter keys
|
||||
*/
|
||||
|
||||
export function useGetRuleHistoryFilterKeys<
|
||||
TData = Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleHistoryFilterKeysPathParameters,
|
||||
params?: GetRuleHistoryFilterKeysParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryFilterKeys>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetRuleHistoryFilterKeysQueryOptions(
|
||||
{ id },
|
||||
params,
|
||||
options,
|
||||
);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get rule history filter keys
|
||||
*/
|
||||
export const invalidateGetRuleHistoryFilterKeys = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetRuleHistoryFilterKeysPathParameters,
|
||||
params?: GetRuleHistoryFilterKeysParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetRuleHistoryFilterKeysQueryKey({ id }, params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns distinct label values for a given key from rule history entries.
|
||||
* @summary Get rule history filter values
|
||||
*/
|
||||
export const getRuleHistoryFilterValues = (
|
||||
{ id }: GetRuleHistoryFilterValuesPathParameters,
|
||||
params?: GetRuleHistoryFilterValuesParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetRuleHistoryFilterValues200>({
|
||||
url: `/api/v2/rules/${id}/history/filter_values`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetRuleHistoryFilterValuesQueryKey = (
|
||||
{ id }: GetRuleHistoryFilterValuesPathParameters,
|
||||
params?: GetRuleHistoryFilterValuesParams,
|
||||
) => {
|
||||
return [
|
||||
`/api/v2/rules/${id}/history/filter_values`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getGetRuleHistoryFilterValuesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleHistoryFilterValuesPathParameters,
|
||||
params?: GetRuleHistoryFilterValuesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ??
|
||||
getGetRuleHistoryFilterValuesQueryKey({ id }, params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>
|
||||
> = ({ signal }) => getRuleHistoryFilterValues({ id }, params, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetRuleHistoryFilterValuesQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>
|
||||
>;
|
||||
export type GetRuleHistoryFilterValuesQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get rule history filter values
|
||||
*/
|
||||
|
||||
export function useGetRuleHistoryFilterValues<
|
||||
TData = Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleHistoryFilterValuesPathParameters,
|
||||
params?: GetRuleHistoryFilterValuesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryFilterValues>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetRuleHistoryFilterValuesQueryOptions(
|
||||
{ id },
|
||||
params,
|
||||
options,
|
||||
);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get rule history filter values
|
||||
*/
|
||||
export const invalidateGetRuleHistoryFilterValues = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetRuleHistoryFilterValuesPathParameters,
|
||||
params?: GetRuleHistoryFilterValuesParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetRuleHistoryFilterValuesQueryKey({ id }, params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns overall firing/inactive intervals for a rule in the selected time range.
|
||||
* @summary Get rule overall status timeline
|
||||
*/
|
||||
export const getRuleHistoryOverallStatus = (
|
||||
{ id }: GetRuleHistoryOverallStatusPathParameters,
|
||||
params: GetRuleHistoryOverallStatusParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetRuleHistoryOverallStatus200>({
|
||||
url: `/api/v2/rules/${id}/history/overall_status`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetRuleHistoryOverallStatusQueryKey = (
|
||||
{ id }: GetRuleHistoryOverallStatusPathParameters,
|
||||
params?: GetRuleHistoryOverallStatusParams,
|
||||
) => {
|
||||
return [
|
||||
`/api/v2/rules/${id}/history/overall_status`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getGetRuleHistoryOverallStatusQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleHistoryOverallStatusPathParameters,
|
||||
params: GetRuleHistoryOverallStatusParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ??
|
||||
getGetRuleHistoryOverallStatusQueryKey({ id }, params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>
|
||||
> = ({ signal }) => getRuleHistoryOverallStatus({ id }, params, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetRuleHistoryOverallStatusQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>
|
||||
>;
|
||||
export type GetRuleHistoryOverallStatusQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get rule overall status timeline
|
||||
*/
|
||||
|
||||
export function useGetRuleHistoryOverallStatus<
|
||||
TData = Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleHistoryOverallStatusPathParameters,
|
||||
params: GetRuleHistoryOverallStatusParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryOverallStatus>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetRuleHistoryOverallStatusQueryOptions(
|
||||
{ id },
|
||||
params,
|
||||
options,
|
||||
);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get rule overall status timeline
|
||||
*/
|
||||
export const invalidateGetRuleHistoryOverallStatus = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetRuleHistoryOverallStatusPathParameters,
|
||||
params: GetRuleHistoryOverallStatusParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetRuleHistoryOverallStatusQueryKey({ id }, params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns trigger and resolution statistics for a rule in the selected time range.
|
||||
* @summary Get rule history stats
|
||||
*/
|
||||
export const getRuleHistoryStats = (
|
||||
{ id }: GetRuleHistoryStatsPathParameters,
|
||||
params: GetRuleHistoryStatsParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetRuleHistoryStats200>({
|
||||
url: `/api/v2/rules/${id}/history/stats`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetRuleHistoryStatsQueryKey = (
|
||||
{ id }: GetRuleHistoryStatsPathParameters,
|
||||
params?: GetRuleHistoryStatsParams,
|
||||
) => {
|
||||
return [
|
||||
`/api/v2/rules/${id}/history/stats`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getGetRuleHistoryStatsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getRuleHistoryStats>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleHistoryStatsPathParameters,
|
||||
params: GetRuleHistoryStatsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryStats>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetRuleHistoryStatsQueryKey({ id }, params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getRuleHistoryStats>>
|
||||
> = ({ signal }) => getRuleHistoryStats({ id }, params, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryStats>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetRuleHistoryStatsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getRuleHistoryStats>>
|
||||
>;
|
||||
export type GetRuleHistoryStatsQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get rule history stats
|
||||
*/
|
||||
|
||||
export function useGetRuleHistoryStats<
|
||||
TData = Awaited<ReturnType<typeof getRuleHistoryStats>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleHistoryStatsPathParameters,
|
||||
params: GetRuleHistoryStatsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryStats>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetRuleHistoryStatsQueryOptions(
|
||||
{ id },
|
||||
params,
|
||||
options,
|
||||
);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get rule history stats
|
||||
*/
|
||||
export const invalidateGetRuleHistoryStats = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetRuleHistoryStatsPathParameters,
|
||||
params: GetRuleHistoryStatsParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetRuleHistoryStatsQueryKey({ id }, params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns paginated timeline entries for rule state transitions.
|
||||
* @summary Get rule history timeline
|
||||
*/
|
||||
export const getRuleHistoryTimeline = (
|
||||
{ id }: GetRuleHistoryTimelinePathParameters,
|
||||
params: GetRuleHistoryTimelineParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetRuleHistoryTimeline200>({
|
||||
url: `/api/v2/rules/${id}/history/timeline`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetRuleHistoryTimelineQueryKey = (
|
||||
{ id }: GetRuleHistoryTimelinePathParameters,
|
||||
params?: GetRuleHistoryTimelineParams,
|
||||
) => {
|
||||
return [
|
||||
`/api/v2/rules/${id}/history/timeline`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getGetRuleHistoryTimelineQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleHistoryTimelinePathParameters,
|
||||
params: GetRuleHistoryTimelineParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetRuleHistoryTimelineQueryKey({ id }, params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getRuleHistoryTimeline>>
|
||||
> = ({ signal }) => getRuleHistoryTimeline({ id }, params, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetRuleHistoryTimelineQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getRuleHistoryTimeline>>
|
||||
>;
|
||||
export type GetRuleHistoryTimelineQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get rule history timeline
|
||||
*/
|
||||
|
||||
export function useGetRuleHistoryTimeline<
|
||||
TData = Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleHistoryTimelinePathParameters,
|
||||
params: GetRuleHistoryTimelineParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryTimeline>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetRuleHistoryTimelineQueryOptions(
|
||||
{ id },
|
||||
params,
|
||||
options,
|
||||
);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get rule history timeline
|
||||
*/
|
||||
export const invalidateGetRuleHistoryTimeline = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetRuleHistoryTimelinePathParameters,
|
||||
params: GetRuleHistoryTimelineParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetRuleHistoryTimelineQueryKey({ id }, params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns top label combinations contributing to rule firing in the selected time range.
|
||||
* @summary Get top contributors to rule firing
|
||||
*/
|
||||
export const getRuleHistoryTopContributors = (
|
||||
{ id }: GetRuleHistoryTopContributorsPathParameters,
|
||||
params: GetRuleHistoryTopContributorsParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetRuleHistoryTopContributors200>({
|
||||
url: `/api/v2/rules/${id}/history/top_contributors`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetRuleHistoryTopContributorsQueryKey = (
|
||||
{ id }: GetRuleHistoryTopContributorsPathParameters,
|
||||
params?: GetRuleHistoryTopContributorsParams,
|
||||
) => {
|
||||
return [
|
||||
`/api/v2/rules/${id}/history/top_contributors`,
|
||||
...(params ? [params] : []),
|
||||
] as const;
|
||||
};
|
||||
|
||||
export const getGetRuleHistoryTopContributorsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleHistoryTopContributorsPathParameters,
|
||||
params: GetRuleHistoryTopContributorsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ??
|
||||
getGetRuleHistoryTopContributorsQueryKey({ id }, params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>
|
||||
> = ({ signal }) => getRuleHistoryTopContributors({ id }, params, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetRuleHistoryTopContributorsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>
|
||||
>;
|
||||
export type GetRuleHistoryTopContributorsQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get top contributors to rule firing
|
||||
*/
|
||||
|
||||
export function useGetRuleHistoryTopContributors<
|
||||
TData = Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>
|
||||
>(
|
||||
{ id }: GetRuleHistoryTopContributorsPathParameters,
|
||||
params: GetRuleHistoryTopContributorsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getRuleHistoryTopContributors>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetRuleHistoryTopContributorsQueryOptions(
|
||||
{ id },
|
||||
params,
|
||||
options,
|
||||
);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get top contributors to rule firing
|
||||
*/
|
||||
export const invalidateGetRuleHistoryTopContributors = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetRuleHistoryTopContributorsPathParameters,
|
||||
params: GetRuleHistoryTopContributorsParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetRuleHistoryTopContributorsQueryKey({ id }, params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import ROUTES from 'constants/routes';
|
||||
import useUpdatedQuery from 'container/GridCardLayout/useResolveQuery';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
@@ -79,7 +79,7 @@ export function useNavigateToExplorer(): (
|
||||
);
|
||||
|
||||
const { getUpdatedQuery } = useUpdatedQuery();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
return useCallback(
|
||||
|
||||
@@ -116,7 +116,12 @@ describe.each([
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(screen.getByText('FORMAT')).toBeInTheDocument();
|
||||
expect(screen.getByText('Number of Rows')).toBeInTheDocument();
|
||||
expect(screen.getByText('Columns')).toBeInTheDocument();
|
||||
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
expect(screen.queryByText('Columns')).not.toBeInTheDocument();
|
||||
} else {
|
||||
expect(screen.getByText('Columns')).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('allows changing export format', () => {
|
||||
@@ -146,6 +151,17 @@ describe.each([
|
||||
});
|
||||
|
||||
it('allows changing columns scope', () => {
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
renderWithStore(dataSource);
|
||||
fireEvent.click(screen.getByTestId(testId));
|
||||
|
||||
expect(screen.queryByRole('radio', { name: 'All' })).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('radio', { name: 'Selected' }),
|
||||
).not.toBeInTheDocument();
|
||||
return;
|
||||
}
|
||||
|
||||
renderWithStore(dataSource);
|
||||
fireEvent.click(screen.getByTestId(testId));
|
||||
|
||||
@@ -210,7 +226,12 @@ describe.each([
|
||||
mockUseQueryBuilder.mockReturnValue({ stagedQuery: mockQuery });
|
||||
renderWithStore(dataSource);
|
||||
fireEvent.click(screen.getByTestId(testId));
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'Selected' }));
|
||||
|
||||
// For traces, column scope is always Selected and the radio is hidden
|
||||
if (dataSource !== DataSource.TRACES) {
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'Selected' }));
|
||||
}
|
||||
|
||||
fireEvent.click(screen.getByText('Export'));
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -227,6 +248,11 @@ describe.each([
|
||||
});
|
||||
|
||||
it('sends no selectFields when column scope is All', async () => {
|
||||
// For traces, column scope is always Selected — this test only applies to other sources
|
||||
if (dataSource === DataSource.TRACES) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderWithStore(dataSource);
|
||||
fireEvent.click(screen.getByTestId(testId));
|
||||
fireEvent.click(screen.getByRole('radio', { name: 'All' }));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { Button, Popover, Radio, Tooltip, Typography } from 'antd';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { useExportRawData } from 'hooks/useDownloadOptionsMenu/useDownloadOptionsMenu';
|
||||
import { Download, DownloadIcon, Loader2 } from 'lucide-react';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -14,10 +15,12 @@ import './DownloadOptionsMenu.styles.scss';
|
||||
|
||||
interface DownloadOptionsMenuProps {
|
||||
dataSource: DataSource;
|
||||
selectedColumns?: TelemetryFieldKey[];
|
||||
}
|
||||
|
||||
export default function DownloadOptionsMenu({
|
||||
dataSource,
|
||||
selectedColumns,
|
||||
}: DownloadOptionsMenuProps): JSX.Element {
|
||||
const [exportFormat, setExportFormat] = useState<string>(DownloadFormats.CSV);
|
||||
const [rowLimit, setRowLimit] = useState<number>(DownloadRowCounts.TEN_K);
|
||||
@@ -35,9 +38,19 @@ export default function DownloadOptionsMenu({
|
||||
await handleExportRawData({
|
||||
format: exportFormat,
|
||||
rowLimit,
|
||||
clearSelectColumns: columnsScope === DownloadColumnsScopes.ALL,
|
||||
clearSelectColumns:
|
||||
dataSource !== DataSource.TRACES &&
|
||||
columnsScope === DownloadColumnsScopes.ALL,
|
||||
selectedColumns,
|
||||
});
|
||||
}, [exportFormat, rowLimit, columnsScope, handleExportRawData]);
|
||||
}, [
|
||||
exportFormat,
|
||||
rowLimit,
|
||||
columnsScope,
|
||||
selectedColumns,
|
||||
handleExportRawData,
|
||||
dataSource,
|
||||
]);
|
||||
|
||||
const popoverContent = useMemo(
|
||||
() => (
|
||||
@@ -72,18 +85,22 @@ export default function DownloadOptionsMenu({
|
||||
</Radio.Group>
|
||||
</div>
|
||||
|
||||
<div className="horizontal-line" />
|
||||
{dataSource !== DataSource.TRACES && (
|
||||
<>
|
||||
<div className="horizontal-line" />
|
||||
|
||||
<div className="columns-scope">
|
||||
<Typography.Text className="title">Columns</Typography.Text>
|
||||
<Radio.Group
|
||||
value={columnsScope}
|
||||
onChange={(e): void => setColumnsScope(e.target.value)}
|
||||
>
|
||||
<Radio value={DownloadColumnsScopes.ALL}>All</Radio>
|
||||
<Radio value={DownloadColumnsScopes.SELECTED}>Selected</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
<div className="columns-scope">
|
||||
<Typography.Text className="title">Columns</Typography.Text>
|
||||
<Radio.Group
|
||||
value={columnsScope}
|
||||
onChange={(e): void => setColumnsScope(e.target.value)}
|
||||
>
|
||||
<Radio value={DownloadColumnsScopes.ALL}>All</Radio>
|
||||
<Radio value={DownloadColumnsScopes.SELECTED}>Selected</Radio>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
@@ -97,7 +114,14 @@ export default function DownloadOptionsMenu({
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
[exportFormat, rowLimit, columnsScope, isDownloading, handleExport],
|
||||
[
|
||||
exportFormat,
|
||||
rowLimit,
|
||||
columnsScope,
|
||||
isDownloading,
|
||||
handleExport,
|
||||
dataSource,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { Badge } from '@signozhq/badge';
|
||||
import { Button } from '@signozhq/button';
|
||||
import { DialogFooter, DialogWrapper } from '@signozhq/dialog';
|
||||
@@ -20,7 +21,7 @@ import { RenderErrorResponseDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
getResetPasswordToken,
|
||||
useDeleteUser,
|
||||
useUpdateUser,
|
||||
useUpdateUserDeprecated,
|
||||
} from 'api/generated/services/users';
|
||||
import { AxiosError } from 'axios';
|
||||
import { MemberRow } from 'components/MembersTable/MembersTable';
|
||||
@@ -60,7 +61,7 @@ function EditMemberDrawer({
|
||||
|
||||
const isInvited = member?.status === MemberStatus.Invited;
|
||||
|
||||
const { mutate: updateUser, isLoading: isSaving } = useUpdateUser({
|
||||
const { mutate: updateUser, isLoading: isSaving } = useUpdateUserDeprecated({
|
||||
mutation: {
|
||||
onSuccess: (): void => {
|
||||
toast.success('Member details updated successfully', { richColors: true });
|
||||
@@ -177,26 +178,30 @@ function EditMemberDrawer({
|
||||
}
|
||||
}, [member, isInvited, setLinkType, onClose]);
|
||||
|
||||
const [copyState, copyToClipboard] = useCopyToClipboard();
|
||||
const handleCopyResetLink = useCallback(async (): Promise<void> => {
|
||||
if (!resetLink) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(resetLink);
|
||||
setHasCopiedResetLink(true);
|
||||
setTimeout(() => setHasCopiedResetLink(false), 2000);
|
||||
toast.success(
|
||||
linkType === 'invite'
|
||||
? 'Invite link copied to clipboard'
|
||||
: 'Reset link copied to clipboard',
|
||||
{ richColors: true },
|
||||
);
|
||||
} catch {
|
||||
copyToClipboard(resetLink);
|
||||
|
||||
setHasCopiedResetLink(true);
|
||||
setTimeout(() => setHasCopiedResetLink(false), 2000);
|
||||
toast.success(
|
||||
linkType === 'invite'
|
||||
? 'Invite link copied to clipboard'
|
||||
: 'Reset link copied to clipboard',
|
||||
{ richColors: true },
|
||||
);
|
||||
}, [resetLink, copyToClipboard, linkType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (copyState.error) {
|
||||
toast.error('Failed to copy link', {
|
||||
richColors: true,
|
||||
});
|
||||
}
|
||||
}, [resetLink, linkType]);
|
||||
}, [copyState.error]);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
setShowDeleteConfirm(false);
|
||||
|
||||
@@ -4,16 +4,10 @@ import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import {
|
||||
getResetPasswordToken,
|
||||
useDeleteUser,
|
||||
useUpdateUser,
|
||||
useUpdateUserDeprecated,
|
||||
} from 'api/generated/services/users';
|
||||
import { MemberStatus } from 'container/MembersSettings/utils';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
userEvent,
|
||||
waitFor,
|
||||
} from 'tests/test-utils';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { ROLES } from 'types/roles';
|
||||
|
||||
import EditMemberDrawer, { EditMemberDrawerProps } from '../EditMemberDrawer';
|
||||
@@ -50,7 +44,7 @@ jest.mock('@signozhq/dialog', () => ({
|
||||
|
||||
jest.mock('api/generated/services/users', () => ({
|
||||
useDeleteUser: jest.fn(),
|
||||
useUpdateUser: jest.fn(),
|
||||
useUpdateUserDeprecated: jest.fn(),
|
||||
getResetPasswordToken: jest.fn(),
|
||||
}));
|
||||
|
||||
@@ -65,6 +59,16 @@ jest.mock('@signozhq/sonner', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const mockCopyToClipboard = jest.fn();
|
||||
const mockCopyState = { value: undefined, error: undefined };
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
useCopyToClipboard: (): [typeof mockCopyState, typeof mockCopyToClipboard] => [
|
||||
mockCopyState,
|
||||
mockCopyToClipboard,
|
||||
],
|
||||
}));
|
||||
|
||||
const mockUpdateMutate = jest.fn();
|
||||
const mockDeleteMutate = jest.fn();
|
||||
const mockGetResetPasswordToken = jest.mocked(getResetPasswordToken);
|
||||
@@ -105,7 +109,7 @@ function renderDrawer(
|
||||
describe('EditMemberDrawer', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(useUpdateUser as jest.Mock).mockReturnValue({
|
||||
(useUpdateUserDeprecated as jest.Mock).mockReturnValue({
|
||||
mutate: mockUpdateMutate,
|
||||
isLoading: false,
|
||||
});
|
||||
@@ -130,7 +134,7 @@ describe('EditMemberDrawer', () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
(useUpdateUser as jest.Mock).mockImplementation((options) => ({
|
||||
(useUpdateUserDeprecated as jest.Mock).mockImplementation((options) => ({
|
||||
mutate: mockUpdateMutate.mockImplementation(() => {
|
||||
options?.mutation?.onSuccess?.();
|
||||
}),
|
||||
@@ -239,7 +243,7 @@ describe('EditMemberDrawer', () => {
|
||||
const onComplete = jest.fn();
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
(useUpdateUser as jest.Mock).mockImplementation((options) => ({
|
||||
(useUpdateUserDeprecated as jest.Mock).mockImplementation((options) => ({
|
||||
mutate: mockUpdateMutate.mockImplementation(() => {
|
||||
options?.mutation?.onSuccess?.();
|
||||
}),
|
||||
@@ -280,7 +284,7 @@ describe('EditMemberDrawer', () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const mockToast = jest.mocked(toast);
|
||||
|
||||
(useUpdateUser as jest.Mock).mockImplementation((options) => ({
|
||||
(useUpdateUserDeprecated as jest.Mock).mockImplementation((options) => ({
|
||||
mutate: mockUpdateMutate.mockImplementation(() => {
|
||||
options?.mutation?.onError?.({});
|
||||
}),
|
||||
@@ -361,32 +365,14 @@ describe('EditMemberDrawer', () => {
|
||||
});
|
||||
|
||||
describe('Generate Password Reset Link', () => {
|
||||
const mockWriteText = jest.fn().mockResolvedValue(undefined);
|
||||
let clipboardSpy: jest.SpyInstance | undefined;
|
||||
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText: (): Promise<void> => Promise.resolve() },
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockWriteText.mockClear();
|
||||
clipboardSpy = jest
|
||||
.spyOn(navigator.clipboard, 'writeText')
|
||||
.mockImplementation(mockWriteText);
|
||||
mockCopyToClipboard.mockClear();
|
||||
mockGetResetPasswordToken.mockResolvedValue({
|
||||
status: 'success',
|
||||
data: { token: 'reset-tok-abc', id: 'user-1' },
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clipboardSpy?.mockRestore();
|
||||
});
|
||||
|
||||
it('calls getResetPasswordToken and opens the reset link dialog with the generated link', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
@@ -421,7 +407,7 @@ describe('EditMemberDrawer', () => {
|
||||
});
|
||||
expect(dialog).toHaveTextContent('reset-tok-abc');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /^copy$/i }));
|
||||
await user.click(screen.getByRole('button', { name: /^copy$/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockToast.success).toHaveBeenCalledWith(
|
||||
@@ -430,7 +416,7 @@ describe('EditMemberDrawer', () => {
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockWriteText).toHaveBeenCalledWith(
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith(
|
||||
expect.stringContaining('reset-tok-abc'),
|
||||
);
|
||||
expect(screen.getByRole('button', { name: /copied!/i })).toBeInTheDocument();
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
ScrollText,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
@@ -52,15 +53,18 @@ import {
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { convertFiltersToExpression } from '../QueryBuilderV2/utils';
|
||||
import { VIEW_TYPES, VIEWS } from './constants';
|
||||
import Containers from './Containers/Containers';
|
||||
import { HostDetailProps } from './HostMetricDetail.interfaces';
|
||||
import HostMetricLogsDetailedView from './HostMetricsLogs/HostMetricLogsDetailedView';
|
||||
import { HOST_METRICS_LOGS_EXPR_QUERY_KEY } from './HostMetricsLogs/constants';
|
||||
import HostMetricsLogs from './HostMetricsLogs/HostMetricsLogs';
|
||||
import HostMetricTraces from './HostMetricTraces/HostMetricTraces';
|
||||
import Metrics from './Metrics/Metrics';
|
||||
import Processes from './Processes/Processes';
|
||||
|
||||
import './HostMetricsDetail.styles.scss';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function HostMetricsDetails({
|
||||
host,
|
||||
@@ -129,10 +133,6 @@ function HostMetricsDetails({
|
||||
};
|
||||
}, [host?.hostName, searchParams]);
|
||||
|
||||
const [logFilters, setLogFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialFilters,
|
||||
);
|
||||
|
||||
const [tracesFilters, setTracesFilters] = useState<IBuilderQuery['filters']>(
|
||||
initialFilters,
|
||||
);
|
||||
@@ -147,7 +147,6 @@ function HostMetricsDetails({
|
||||
}, [host]);
|
||||
|
||||
useEffect(() => {
|
||||
setLogFilters(initialFilters);
|
||||
setTracesFilters(initialFilters);
|
||||
}, [initialFilters]);
|
||||
|
||||
@@ -172,7 +171,6 @@ function HostMetricsDetails({
|
||||
setSearchParams({
|
||||
...Object.fromEntries(searchParams.entries()),
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: e.target.value,
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(null),
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.TRACES_FILTERS]: JSON.stringify(null),
|
||||
});
|
||||
}
|
||||
@@ -210,48 +208,30 @@ function HostMetricsDetails({
|
||||
[],
|
||||
);
|
||||
|
||||
const handleChangeLogFilters = useCallback(
|
||||
(value: IBuilderQuery['filters'], view: VIEWS) => {
|
||||
setLogFilters((prevFilters) => {
|
||||
const hostNameFilter = prevFilters?.items?.find(
|
||||
(item) => item.key?.key === 'host.name',
|
||||
);
|
||||
const paginationFilter = value?.items?.find(
|
||||
(item) => item.key?.key === 'id',
|
||||
);
|
||||
const newFilters = value?.items?.filter(
|
||||
(item) => item.key?.key !== 'id' && item.key?.key !== 'host.name',
|
||||
);
|
||||
const initialLogsExpression = useMemo(
|
||||
() =>
|
||||
convertFiltersToExpression({
|
||||
items: [
|
||||
{
|
||||
id: uuidv4(),
|
||||
key: {
|
||||
key: 'host.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
id: 'host.name--string--resource--false',
|
||||
},
|
||||
op: '=',
|
||||
value: host?.hostName || '',
|
||||
},
|
||||
],
|
||||
op: 'AND',
|
||||
}).expression,
|
||||
[host?.hostName],
|
||||
);
|
||||
|
||||
if (newFilters && newFilters?.length > 0) {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
view: InfraMonitoringEvents.LogsView,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
});
|
||||
}
|
||||
|
||||
const updatedFilters = {
|
||||
op: 'AND',
|
||||
items: [
|
||||
hostNameFilter,
|
||||
...(newFilters || []),
|
||||
...(paginationFilter ? [paginationFilter] : []),
|
||||
].filter((item): item is TagFilterItem => item !== undefined),
|
||||
};
|
||||
|
||||
setSearchParams({
|
||||
...Object.fromEntries(searchParams.entries()),
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.LOG_FILTERS]: JSON.stringify(
|
||||
updatedFilters,
|
||||
),
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.VIEW]: view,
|
||||
});
|
||||
return updatedFilters;
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[],
|
||||
const [hostMetricLogsExpr] = useQueryState(
|
||||
HOST_METRICS_LOGS_EXPR_QUERY_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
const handleChangeTracesFilters = useCallback(
|
||||
@@ -308,11 +288,6 @@ function HostMetricsDetails({
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const filtersWithoutPagination = {
|
||||
...logFilters,
|
||||
items: logFilters?.items?.filter((item) => item.key?.key !== 'id') || [],
|
||||
};
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
@@ -322,7 +297,11 @@ function HostMetricsDetails({
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.logs,
|
||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||
filters: filtersWithoutPagination,
|
||||
filter: { expression: hostMetricLogsExpr },
|
||||
expression: hostMetricLogsExpr,
|
||||
having: {
|
||||
expression: '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -564,12 +543,11 @@ function HostMetricsDetails({
|
||||
/>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<HostMetricLogsDetailedView
|
||||
<HostMetricsLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
handleTimeChange={handleTimeChange}
|
||||
handleChangeLogFilters={handleChangeLogFilters}
|
||||
logFilters={logFilters}
|
||||
initialExpression={initialLogsExpression}
|
||||
selectedInterval={selectedInterval}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: var(--spacing-4) 0px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.logs {
|
||||
border: 1px solid var(--border);
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.listContainer {
|
||||
flex: 1;
|
||||
height: calc(100vh - 278px) !important;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
:global(.raw-log-content) {
|
||||
width: 100%;
|
||||
text-wrap: inherit;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.listCard {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
|
||||
:global(.ant-card-body) {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.logsLoadingSkeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
|
||||
:global(.ant-skeleton-input-sm) {
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.noLogsFound {
|
||||
height: 50vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
|
||||
p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
.host-metrics-logs-container {
|
||||
margin-top: 1rem;
|
||||
|
||||
.filter-section {
|
||||
flex: 1;
|
||||
|
||||
.ant-select-selector {
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--bg-slate-400) !important;
|
||||
background-color: var(--bg-ink-300) !important;
|
||||
|
||||
input {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ant-tag .ant-typography {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-metrics-logs-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
|
||||
padding: 12px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--bg-slate-500);
|
||||
}
|
||||
|
||||
.host-metrics-logs {
|
||||
margin-top: 1rem;
|
||||
|
||||
.virtuoso-list {
|
||||
overflow-y: hidden !important;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0.3rem;
|
||||
height: 0.3rem;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--bg-slate-300);
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--bg-slate-200);
|
||||
}
|
||||
|
||||
.ant-row {
|
||||
width: fit-content;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton-container {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-metrics-logs-list-container {
|
||||
flex: 1;
|
||||
height: calc(100vh - 272px) !important;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
|
||||
.raw-log-content {
|
||||
width: 100%;
|
||||
text-wrap: inherit;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.host-metrics-logs-list-card {
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
|
||||
.ant-card-body {
|
||||
padding: 0;
|
||||
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.logs-loading-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 8px 0;
|
||||
|
||||
.ant-skeleton-input-sm {
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-logs-found {
|
||||
height: 50vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
|
||||
.ant-typography {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.filter-section {
|
||||
border-top: 1px solid var(--bg-vanilla-300);
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
|
||||
.ant-select-selector {
|
||||
border-color: var(--bg-vanilla-300) !important;
|
||||
background-color: var(--bg-vanilla-100) !important;
|
||||
color: var(--bg-ink-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import QueryBuilderSearch from 'container/QueryBuilder/filters/QueryBuilderSearch';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { VIEWS } from '../constants';
|
||||
import HostMetricsLogs from './HostMetricsLogs';
|
||||
|
||||
import './HostMetricLogs.styles.scss';
|
||||
|
||||
interface Props {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
handleChangeLogFilters: (value: IBuilderQuery['filters'], view: VIEWS) => void;
|
||||
logFilters: IBuilderQuery['filters'];
|
||||
selectedInterval: Time;
|
||||
}
|
||||
|
||||
function HostMetricLogsDetailedView({
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
handleChangeLogFilters,
|
||||
logFilters,
|
||||
selectedInterval,
|
||||
}: Props): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const updatedCurrentQuery = useMemo(
|
||||
() => ({
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
{
|
||||
...currentQuery.builder.queryData[0],
|
||||
dataSource: DataSource.LOGS,
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
...currentQuery.builder.queryData[0].aggregateAttribute,
|
||||
},
|
||||
filters: {
|
||||
items:
|
||||
logFilters?.items?.filter((item) => item.key?.key !== 'host.name') ||
|
||||
[],
|
||||
op: 'AND',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
[currentQuery, logFilters?.items],
|
||||
);
|
||||
|
||||
const query = updatedCurrentQuery?.builder?.queryData[0] || null;
|
||||
|
||||
return (
|
||||
<div className="host-metrics-logs-container">
|
||||
<div className="host-metrics-logs-header">
|
||||
<div className="filter-section">
|
||||
{query && (
|
||||
<QueryBuilderSearch
|
||||
query={query as IBuilderQuery}
|
||||
onChange={(value): void => handleChangeLogFilters(value, VIEWS.LOGS)}
|
||||
disableNavigationShortcuts
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="datetime-section">
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<HostMetricsLogs timeRange={timeRange} filters={logFilters} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default HostMetricLogsDetailedView;
|
||||
@@ -1,88 +1,161 @@
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
import { Card } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import LogDetail from 'components/LogDetail';
|
||||
import RawLogView from 'components/Logs/RawLogView';
|
||||
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
|
||||
import { DEFAULT_ENTITY_VERSION } from 'constants/app';
|
||||
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import LogsError from 'container/LogsError/LogsError';
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import { useHandleLogsPagination } from 'hooks/infraMonitoring/useHandleLogsPagination';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
|
||||
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
|
||||
import useScrollToLog from 'hooks/logs/useScrollToLog';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import { getHostLogsQueryPayload } from './constants';
|
||||
import {
|
||||
getHostLogsQueryPayload,
|
||||
HOST_METRICS_LOGS_EXPR_QUERY_KEY,
|
||||
} from './constants';
|
||||
import { useInfiniteHostMetricLogs } from './hooks';
|
||||
import NoLogsContainer from './NoLogsContainer';
|
||||
|
||||
import './HostMetricLogs.styles.scss';
|
||||
import styles from './HostMetricLogs.module.scss';
|
||||
|
||||
interface Props {
|
||||
initialExpression: string;
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
filters: IBuilderQuery['filters'];
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
}
|
||||
|
||||
function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
|
||||
const EXPRESSION_DEBOUNCE_TIME_MS = 300;
|
||||
|
||||
function HostMetricsLogs({
|
||||
initialExpression,
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
selectedInterval,
|
||||
}: Props): JSX.Element {
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
|
||||
const [filterExpression, setFilterExpression] = useQueryState(
|
||||
HOST_METRICS_LOGS_EXPR_QUERY_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
const [inputExpression, setInputExpression] = useState(
|
||||
filterExpression || initialExpression,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// If expression is present in the URL, prefer it and don't override it.
|
||||
// Otherwise, initialize URL state from the host's default expression.
|
||||
if (filterExpression) {
|
||||
setInputExpression(filterExpression);
|
||||
return;
|
||||
}
|
||||
|
||||
setInputExpression(initialExpression);
|
||||
setFilterExpression(initialExpression);
|
||||
}, [filterExpression, initialExpression, setFilterExpression]);
|
||||
|
||||
const debouncedFilterExpression = useDebounce(
|
||||
filterExpression?.trim() || initialExpression,
|
||||
EXPRESSION_DEBOUNCE_TIME_MS,
|
||||
);
|
||||
|
||||
const {
|
||||
activeLog,
|
||||
onAddToQuery,
|
||||
selectedTab,
|
||||
handleSetActiveLog,
|
||||
handleCloseLogDetail,
|
||||
} = useLogDetailHandlers();
|
||||
|
||||
const basePayload = getHostLogsQueryPayload(
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
filters,
|
||||
const onAddToQuery = useCallback(
|
||||
(fieldKey: string, fieldValue: string, operator: string): void => {
|
||||
handleCloseLogDetail();
|
||||
|
||||
const partExpression = generateFilterQuery({
|
||||
fieldKey,
|
||||
fieldValue,
|
||||
type: getOldLogsOperatorFromNew(operator),
|
||||
});
|
||||
|
||||
const newExpression = inputExpression.trim()
|
||||
? `${inputExpression} AND ${partExpression}`
|
||||
: partExpression;
|
||||
|
||||
setInputExpression(newExpression);
|
||||
setFilterExpression(newExpression);
|
||||
},
|
||||
[inputExpression, setFilterExpression, handleCloseLogDetail],
|
||||
);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
(expression: string): void => {
|
||||
setInputExpression(expression);
|
||||
|
||||
const validation = validateQuery(expression);
|
||||
if (validation.isValid) {
|
||||
setFilterExpression(expression);
|
||||
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
view: InfraMonitoringEvents.LogsView,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
});
|
||||
}
|
||||
},
|
||||
[setFilterExpression],
|
||||
);
|
||||
|
||||
const queryData = useMemo(
|
||||
() =>
|
||||
getHostLogsQueryPayload({
|
||||
start: timeRange.startTime,
|
||||
end: timeRange.endTime,
|
||||
// this should use inputExpression to show suggestions correctly
|
||||
// while we don't accept the final expression yet
|
||||
expression: inputExpression,
|
||||
}).queryData,
|
||||
[timeRange.startTime, timeRange.endTime, inputExpression],
|
||||
);
|
||||
|
||||
const {
|
||||
logs,
|
||||
hasReachedEndOfLogs,
|
||||
isPaginating,
|
||||
currentPage,
|
||||
setIsPaginating,
|
||||
handleNewData,
|
||||
loadMoreLogs,
|
||||
queryPayload,
|
||||
} = useHandleLogsPagination({
|
||||
timeRange,
|
||||
filters,
|
||||
excludeFilterKeys: ['host.name'],
|
||||
basePayload,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isError,
|
||||
} = useInfiniteHostMetricLogs({
|
||||
expression: debouncedFilterExpression,
|
||||
startTime: timeRange.startTime,
|
||||
endTime: timeRange.endTime,
|
||||
});
|
||||
|
||||
const { data, isLoading, isFetching, isError } = useQuery({
|
||||
queryKey: [
|
||||
'hostMetricsLogs',
|
||||
timeRange.startTime,
|
||||
timeRange.endTime,
|
||||
filters,
|
||||
currentPage,
|
||||
],
|
||||
queryFn: () => GetMetricQueryRange(queryPayload, DEFAULT_ENTITY_VERSION),
|
||||
enabled: !!queryPayload,
|
||||
keepPreviousData: isPaginating,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.payload?.data?.newResult?.data?.result) {
|
||||
handleNewData(data.payload.data.newResult.data.result);
|
||||
}
|
||||
}, [data, handleNewData]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsPaginating(false);
|
||||
}, [data, setIsPaginating]);
|
||||
|
||||
const handleScrollToLog = useScrollToLog({
|
||||
logs,
|
||||
virtuosoRef,
|
||||
@@ -122,22 +195,21 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
|
||||
const renderFooter = useCallback(
|
||||
(): JSX.Element | null => (
|
||||
<>
|
||||
{isFetching ? (
|
||||
<div className="logs-loading-skeleton"> Loading more logs ... </div>
|
||||
) : hasReachedEndOfLogs ? (
|
||||
<div className="logs-loading-skeleton"> *** End *** </div>
|
||||
{isFetchingNextPage ? (
|
||||
<div className={styles.logsLoadingSkeleton}> Loading more logs ... </div>
|
||||
) : !hasNextPage && logs.length > 0 ? (
|
||||
<div className={styles.logsLoadingSkeleton}> *** End *** </div>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
[isFetching, hasReachedEndOfLogs],
|
||||
[isFetchingNextPage, hasNextPage, logs.length],
|
||||
);
|
||||
|
||||
const renderContent = useMemo(
|
||||
() => (
|
||||
<Card bordered={false} className="host-metrics-logs-list-card">
|
||||
<Card bordered={false} className={styles.listCard}>
|
||||
<OverlayScrollbar isVirtuoso>
|
||||
<Virtuoso
|
||||
className="host-metrics-logs-virtuoso"
|
||||
key="host-metrics-logs-virtuoso"
|
||||
ref={virtuosoRef}
|
||||
data={logs}
|
||||
@@ -155,32 +227,55 @@ function HostMetricsLogs({ timeRange, filters }: Props): JSX.Element {
|
||||
[logs, loadMoreLogs, getItemContent, renderFooter],
|
||||
);
|
||||
|
||||
const showInitialLoading = isLoading || (isFetching && logs.length === 0);
|
||||
|
||||
return (
|
||||
<div className="host-metrics-logs">
|
||||
{isLoading && <LogsLoading />}
|
||||
{!isLoading && !isError && logs.length === 0 && <NoLogsContainer />}
|
||||
{isError && !isLoading && <LogsError />}
|
||||
{!isLoading && !isError && logs.length > 0 && (
|
||||
<div
|
||||
className="host-metrics-logs-list-container"
|
||||
data-log-detail-ignore="true"
|
||||
>
|
||||
{renderContent}
|
||||
</div>
|
||||
)}
|
||||
{selectedTab && activeLog && (
|
||||
<LogDetail
|
||||
log={activeLog}
|
||||
onClose={handleCloseLogDetail}
|
||||
logs={logs}
|
||||
onNavigateLog={handleSetActiveLog}
|
||||
selectedTab={selectedTab}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onScrollToLog={handleScrollToLog}
|
||||
<>
|
||||
<div className={styles.header}>
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<QuerySearch
|
||||
queryData={queryData}
|
||||
onChange={handleFilterChange}
|
||||
dataSource={DataSource.LOGS}
|
||||
/>
|
||||
|
||||
<div className={styles.logs}>
|
||||
{showInitialLoading && <LogsLoading />}
|
||||
{!showInitialLoading && !isError && logs.length === 0 && (
|
||||
<NoLogsContainer />
|
||||
)}
|
||||
{isError && !showInitialLoading && <LogsError />}
|
||||
{!showInitialLoading && !isError && logs.length > 0 && (
|
||||
<div className={styles.listContainer} data-log-detail-ignore="true">
|
||||
{renderContent}
|
||||
</div>
|
||||
)}
|
||||
{selectedTab && activeLog && (
|
||||
<LogDetail
|
||||
log={activeLog}
|
||||
onClose={handleCloseLogDetail}
|
||||
logs={logs}
|
||||
onNavigateLog={handleSetActiveLog}
|
||||
selectedTab={selectedTab}
|
||||
onAddToQuery={onAddToQuery}
|
||||
onClickActionItem={onAddToQuery}
|
||||
onScrollToLog={handleScrollToLog}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Typography } from 'antd';
|
||||
import { Ghost } from 'lucide-react';
|
||||
|
||||
const { Text } = Typography;
|
||||
import styles from './HostMetricLogs.module.scss';
|
||||
|
||||
export default function NoLogsContainer(): React.ReactElement {
|
||||
return (
|
||||
<div className="no-logs-found">
|
||||
<Text type="secondary">
|
||||
<div className={styles.noLogsFound}>
|
||||
<p>
|
||||
<Ghost size={24} color={Color.BG_AMBER_500} /> No logs found for this host
|
||||
in the selected time range.
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,975 @@
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { act, render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
|
||||
import HostMetricsLogs from '../HostMetricsLogs';
|
||||
|
||||
jest.mock('react-virtuoso', () => {
|
||||
const actual = jest.requireActual('react-virtuoso');
|
||||
return {
|
||||
...actual,
|
||||
Virtuoso: ({
|
||||
data,
|
||||
itemContent,
|
||||
endReached,
|
||||
components,
|
||||
className,
|
||||
}: {
|
||||
data?: any[];
|
||||
itemContent?: (index: number, item: any) => React.ReactNode;
|
||||
endReached?: (index: number) => void;
|
||||
components?: { Footer?: React.ComponentType };
|
||||
className?: string;
|
||||
}): JSX.Element => (
|
||||
<div data-testid="virtuoso-mock" className={className}>
|
||||
{Array.isArray(data) &&
|
||||
data.map((item, index) => (
|
||||
<div key={item?.id ?? index} data-testid={`virtuoso-item-${index}`}>
|
||||
{itemContent?.(index, item)}
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
data-testid="virtuoso-end-reached"
|
||||
onClick={(): void => endReached?.((data?.length || 0) - 1)}
|
||||
>
|
||||
endReached
|
||||
</button>
|
||||
{components?.Footer ? <components.Footer /> : null}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
const QUERY_RANGE_URL = `${ENVIRONMENT.baseURL}/api/v5/query_range`;
|
||||
const FIELDS_KEYS_URL = `${ENVIRONMENT.baseURL}/api/v1/fields/keys`;
|
||||
const FIELDS_VALUES_URL = `${ENVIRONMENT.baseURL}/api/v1/fields/values`;
|
||||
|
||||
// Creates a V5 API response structure for raw logs data
|
||||
// The API response is wrapped in { data: { type: '...', data: { results: [...] } } }
|
||||
const createLogsResponse = ({
|
||||
offset = 0,
|
||||
pageSize = 100,
|
||||
hasMore = true,
|
||||
}: {
|
||||
offset?: number;
|
||||
pageSize?: number;
|
||||
hasMore?: boolean;
|
||||
}): any => {
|
||||
const itemsForThisPage = hasMore ? pageSize : Math.min(pageSize / 2, 10);
|
||||
|
||||
return {
|
||||
data: {
|
||||
type: 'raw',
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
queryName: 'A',
|
||||
rows: Array.from({ length: itemsForThisPage }, (_, index) => {
|
||||
const cumulativeIndex = offset + index;
|
||||
const baseTimestamp = new Date('2024-02-15T21:20:22Z').getTime();
|
||||
const currentTimestamp = new Date(
|
||||
baseTimestamp - cumulativeIndex * 1000,
|
||||
);
|
||||
const timestampString = currentTimestamp.toISOString();
|
||||
const id = `log-id-${cumulativeIndex}`;
|
||||
const logLevel = ['INFO', 'WARN', 'ERROR'][cumulativeIndex % 3];
|
||||
const service = ['frontend', 'backend', 'database'][cumulativeIndex % 3];
|
||||
|
||||
return {
|
||||
timestamp: timestampString,
|
||||
data: {
|
||||
attributes_bool: {},
|
||||
attributes_float64: {},
|
||||
attributes_int64: {},
|
||||
attributes_string: {
|
||||
host_name: 'test-host',
|
||||
log_level: logLevel,
|
||||
service,
|
||||
},
|
||||
body: `${timestampString} ${logLevel} ${service} Log message ${cumulativeIndex}`,
|
||||
id,
|
||||
resources_string: {
|
||||
'host.name': 'test-host',
|
||||
},
|
||||
severity_number: [9, 13, 17][cumulativeIndex % 3],
|
||||
severity_text: logLevel,
|
||||
span_id: `span-${cumulativeIndex}`,
|
||||
trace_flags: 0,
|
||||
trace_id: `trace-${cumulativeIndex}`,
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createEmptyLogsResponse = (): any => ({
|
||||
data: {
|
||||
type: 'raw',
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
queryName: 'A',
|
||||
rows: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
initialExpression: 'host_name = "test-host"',
|
||||
timeRange: {
|
||||
startTime: 1708000000,
|
||||
endTime: 1708003600,
|
||||
},
|
||||
isModalTimeSelection: false,
|
||||
handleTimeChange: jest.fn(),
|
||||
selectedInterval: '15m' as const,
|
||||
};
|
||||
|
||||
// Mock OverlayScrollbar to avoid scroll behavior issues in tests
|
||||
jest.mock('components/OverlayScrollbar/OverlayScrollbar', () => ({
|
||||
__esModule: true,
|
||||
default: ({ children }: { children: React.ReactNode }): JSX.Element => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
|
||||
__esModule: true,
|
||||
default: ({
|
||||
onTimeChange,
|
||||
}: {
|
||||
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
|
||||
}): JSX.Element => {
|
||||
return (
|
||||
<div className="datetime-section" data-testid="datetime-selection">
|
||||
<button
|
||||
data-testid="time-picker-btn"
|
||||
onClick={(): void => {
|
||||
onTimeChange?.('5m');
|
||||
}}
|
||||
>
|
||||
Select Time
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
const createFieldKeysResponse = (): any => ({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: {},
|
||||
},
|
||||
});
|
||||
|
||||
const createFieldValuesResponse = (): any => ({
|
||||
status: 'success',
|
||||
data: {
|
||||
values: {
|
||||
stringValues: [],
|
||||
numberValues: [],
|
||||
boolValues: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const renderComponent = (
|
||||
props = defaultProps,
|
||||
searchParams?: Record<string, string>,
|
||||
): ReturnType<typeof render> =>
|
||||
render(
|
||||
<NuqsTestingAdapter searchParams={searchParams} hasMemory>
|
||||
<VirtuosoMockContext.Provider
|
||||
value={{ viewportHeight: 600, itemHeight: 50 }}
|
||||
>
|
||||
<HostMetricsLogs {...props} />
|
||||
</VirtuosoMockContext.Provider>
|
||||
</NuqsTestingAdapter>,
|
||||
);
|
||||
|
||||
describe('HostMetricsLogs', () => {
|
||||
beforeEach(() => {
|
||||
window.history.pushState({}, 'Test', '/');
|
||||
server.use(
|
||||
rest.get(FIELDS_KEYS_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createFieldKeysResponse())),
|
||||
),
|
||||
rest.get(FIELDS_VALUES_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createFieldValuesResponse())),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe('loading state', () => {
|
||||
it('should show loading state while fetching logs', async () => {
|
||||
let resolveRequest: (value: any) => void;
|
||||
const pendingPromise = new Promise((resolve) => {
|
||||
resolveRequest = resolve;
|
||||
});
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (_, res, ctx) => {
|
||||
await pendingPromise;
|
||||
return res(ctx.status(200), ctx.json(createLogsResponse({})));
|
||||
}),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByText('pending_data_placeholder')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
resolveRequest!(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty state', () => {
|
||||
it('should show no logs message when no logs are returned', async () => {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createEmptyLogsResponse())),
|
||||
),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText(/No logs found for this host/i),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error state', () => {
|
||||
it('should show error state when API returns error', async () => {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(ctx.status(500), ctx.json({ error: 'Internal Server Error' })),
|
||||
),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('success state', () => {
|
||||
it('should render logs when API returns data', async () => {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 }))),
|
||||
),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Log message 0/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render initial expression in QuerySearch editor', async () => {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 }))),
|
||||
),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
const editorText =
|
||||
document.querySelector('.query-where-clause-editor')?.textContent || '';
|
||||
expect(editorText).toContain('host_name');
|
||||
expect(editorText).toContain('test-host');
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the filter section', async () => {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 }))),
|
||||
),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
document.querySelector('.code-mirror-where-clause'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render date time selection component', async () => {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 }))),
|
||||
),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
// DateTimeSelectionV2 renders a time picker button
|
||||
expect(document.querySelector('.datetime-section')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
it('should send correct offset for pagination', async () => {
|
||||
const requestPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
requestPayloads.push(payload);
|
||||
|
||||
const querySpec = payload.compositeQuery?.queries?.[0]?.spec;
|
||||
const offset = querySpec?.offset ?? 0;
|
||||
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(
|
||||
createLogsResponse({
|
||||
offset,
|
||||
pageSize: 100,
|
||||
hasMore: offset === 0,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestPayloads.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
const firstPayload = requestPayloads[0];
|
||||
const querySpec = firstPayload.compositeQuery?.queries?.[0]?.spec;
|
||||
expect(querySpec?.offset).toBe(0);
|
||||
});
|
||||
|
||||
it('should fetch next page when virtuoso endReached is triggered', async () => {
|
||||
const requestPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
requestPayloads.push(payload);
|
||||
|
||||
const querySpec = payload.compositeQuery?.queries?.[0]?.spec;
|
||||
const offset = querySpec?.offset ?? 0;
|
||||
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(
|
||||
createLogsResponse({
|
||||
offset,
|
||||
pageSize: 100,
|
||||
hasMore: offset === 0,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Log message 0/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(requestPayloads[0]?.compositeQuery?.queries?.[0]?.spec?.offset).toBe(
|
||||
0,
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('virtuoso-end-reached'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestPayloads.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
expect(requestPayloads[1]?.compositeQuery?.queries?.[0]?.spec?.offset).toBe(
|
||||
100,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filter expression', () => {
|
||||
it('should include initial expression in the query', async () => {
|
||||
const requestPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
requestPayloads.push(payload);
|
||||
|
||||
return res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 })));
|
||||
}),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestPayloads.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
const firstPayload = requestPayloads[0];
|
||||
const querySpec = firstPayload.compositeQuery?.queries?.[0]?.spec;
|
||||
|
||||
expect(querySpec?.filter?.expression).toContain('host_name = "test-host"');
|
||||
});
|
||||
|
||||
it('should load expression from URL and persist it in the query', async () => {
|
||||
const requestPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
requestPayloads.push(payload);
|
||||
|
||||
const querySpec = payload.compositeQuery?.queries?.[0]?.spec;
|
||||
const offset = querySpec?.offset ?? 0;
|
||||
|
||||
return res(
|
||||
ctx.status(200),
|
||||
ctx.json(
|
||||
createLogsResponse({
|
||||
offset,
|
||||
pageSize: 100,
|
||||
hasMore: offset === 0,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const urlExpression = 'service = "from-url"';
|
||||
|
||||
renderComponent(defaultProps, { hostMetricsLogsExpr: urlExpression });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestPayloads.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
expect(
|
||||
requestPayloads[0]?.compositeQuery?.queries?.[0]?.spec?.filter?.expression,
|
||||
).toContain(urlExpression);
|
||||
|
||||
await userEvent.click(screen.getByTestId('virtuoso-end-reached'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestPayloads.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
expect(
|
||||
requestPayloads[1]?.compositeQuery?.queries?.[0]?.spec?.filter?.expression,
|
||||
).toContain(urlExpression);
|
||||
});
|
||||
|
||||
it('should use custom expression when provided', async () => {
|
||||
const requestPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
requestPayloads.push(payload);
|
||||
|
||||
return res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 })));
|
||||
}),
|
||||
);
|
||||
|
||||
const customExpression = 'service = "custom-service"';
|
||||
|
||||
renderComponent({
|
||||
...defaultProps,
|
||||
initialExpression: customExpression,
|
||||
});
|
||||
|
||||
// Wait for debounce and potential re-renders to settle
|
||||
await waitFor(
|
||||
() => {
|
||||
const hasCustomExpression = requestPayloads.some((payload) => {
|
||||
const querySpec = payload.compositeQuery?.queries?.[0]?.spec;
|
||||
return querySpec?.filter?.expression?.includes('custom-service');
|
||||
});
|
||||
expect(hasCustomExpression).toBe(true);
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('time range', () => {
|
||||
it('should include correct time range in the query', async () => {
|
||||
const requestPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
requestPayloads.push(payload);
|
||||
|
||||
return res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 })));
|
||||
}),
|
||||
);
|
||||
|
||||
const customTimeRange = {
|
||||
startTime: 1700000000,
|
||||
endTime: 1700003600,
|
||||
};
|
||||
|
||||
renderComponent({
|
||||
...defaultProps,
|
||||
timeRange: customTimeRange,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestPayloads.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
const firstPayload = requestPayloads[0];
|
||||
|
||||
// V5 API expects milliseconds (seconds * 1000)
|
||||
expect(firstPayload.start).toBe(customTimeRange.startTime * 1000);
|
||||
expect(firstPayload.end).toBe(customTimeRange.endTime * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('query structure', () => {
|
||||
it('should send correct query structure to the API', async () => {
|
||||
const requestPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
requestPayloads.push(payload);
|
||||
|
||||
return res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 })));
|
||||
}),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestPayloads.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
const firstPayload = requestPayloads[0];
|
||||
const querySpec = firstPayload.compositeQuery?.queries?.[0]?.spec;
|
||||
|
||||
expect(querySpec?.signal).toBe('logs');
|
||||
expect(querySpec?.order).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
key: expect.objectContaining({ name: 'timestamp' }),
|
||||
direction: 'desc',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should send request type as raw for logs list', async () => {
|
||||
const requestPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
requestPayloads.push(payload);
|
||||
|
||||
return res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 })));
|
||||
}),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestPayloads.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
const firstPayload = requestPayloads[0];
|
||||
expect(firstPayload.requestType).toBe('raw');
|
||||
});
|
||||
|
||||
it('should include pageSize in the query', async () => {
|
||||
const requestPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
requestPayloads.push(payload);
|
||||
|
||||
return res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 })));
|
||||
}),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestPayloads.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
const firstPayload = requestPayloads[0];
|
||||
const querySpec = firstPayload.compositeQuery?.queries?.[0]?.spec;
|
||||
|
||||
// Should have a limit set for pagination
|
||||
expect(querySpec?.limit).toBeDefined();
|
||||
expect(typeof querySpec?.limit).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('component props', () => {
|
||||
it('should render datetime section with isModalTimeSelection', async () => {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 }))),
|
||||
),
|
||||
);
|
||||
|
||||
renderComponent({
|
||||
...defaultProps,
|
||||
isModalTimeSelection: true,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.datetime-section')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render component with handleTimeChange', async () => {
|
||||
const mockHandleTimeChange = jest.fn();
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 }))),
|
||||
),
|
||||
);
|
||||
|
||||
renderComponent({
|
||||
...defaultProps,
|
||||
handleTimeChange: mockHandleTimeChange,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.datetime-section')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('log detail interactions', () => {
|
||||
it('should open log detail drawer when clicking on a log', async () => {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 }))),
|
||||
),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
// Wait for logs to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Log message 0/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click on the first log
|
||||
const logElement = screen.getByText(/Log message 0/);
|
||||
await userEvent.click(logElement);
|
||||
|
||||
// Log detail drawer should open - it contains "Log details" title
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Log details')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should close log detail drawer when clicking on the same log again', async () => {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 }))),
|
||||
),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
// Wait for logs to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Log message 0/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click on the first log to open
|
||||
const logElement = screen.getByText(/Log message 0/);
|
||||
await userEvent.click(logElement);
|
||||
|
||||
// Wait for drawer to open
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Log details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click on the same log to close (through the close button)
|
||||
const closeButton = document.querySelector('.ant-drawer-close');
|
||||
if (closeButton) {
|
||||
await userEvent.click(closeButton);
|
||||
}
|
||||
|
||||
// Drawer should close
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Log details')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display log body in detail drawer', async () => {
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 }))),
|
||||
),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
// Wait for logs to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Log message 0/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click on the first log to open drawer
|
||||
const logElement = screen.getByText(/Log message 0/);
|
||||
await userEvent.click(logElement);
|
||||
|
||||
// Wait for drawer to open
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Log details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify the drawer tabs are displayed
|
||||
// The drawer should show the Overview tab
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Overview')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify other tabs are present
|
||||
expect(screen.getByText('JSON')).toBeInTheDocument();
|
||||
expect(screen.getByText('Context')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('log detail filter actions', () => {
|
||||
it('should apply filter-in from log detail and close the drawer', async () => {
|
||||
const requestPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
requestPayloads.push(payload);
|
||||
return res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 })));
|
||||
}),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Log message 0/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText(/Log message 0/));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Log details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const serviceRow = await waitFor(() => {
|
||||
const attributeNameCells = Array.from(
|
||||
document.querySelectorAll('.attribute-name'),
|
||||
);
|
||||
const serviceCell = attributeNameCells.find((cell) =>
|
||||
(cell.textContent || '').toLowerCase().includes('service'),
|
||||
);
|
||||
const row = serviceCell?.closest('tr');
|
||||
if (!row) {
|
||||
throw new Error('Service attribute row not found');
|
||||
}
|
||||
return row;
|
||||
});
|
||||
|
||||
const filterButtons = serviceRow.querySelectorAll('button.filter-btn');
|
||||
expect(filterButtons?.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
await userEvent.click(filterButtons[0] as HTMLButtonElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Log details')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const matched = requestPayloads.some((payload) => {
|
||||
const expression =
|
||||
payload.compositeQuery?.queries?.[0]?.spec?.filter?.expression || '';
|
||||
return (
|
||||
(expression.includes('attributes_string.service') ||
|
||||
expression.includes('service')) &&
|
||||
expression.includes("('frontend')") &&
|
||||
expression.includes('IN')
|
||||
);
|
||||
});
|
||||
expect(matched).toBe(true);
|
||||
},
|
||||
{ timeout: 2500 },
|
||||
);
|
||||
});
|
||||
|
||||
it('should apply filter-out from log detail and close the drawer', async () => {
|
||||
const requestPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
requestPayloads.push(payload);
|
||||
return res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 })));
|
||||
}),
|
||||
);
|
||||
|
||||
renderComponent();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Log message 0/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByText(/Log message 0/));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Log details')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const serviceRow = await waitFor(() => {
|
||||
const attributeNameCells = Array.from(
|
||||
document.querySelectorAll('.attribute-name'),
|
||||
);
|
||||
const serviceCell = attributeNameCells.find((cell) =>
|
||||
(cell.textContent || '').toLowerCase().includes('service'),
|
||||
);
|
||||
const row = serviceCell?.closest('tr');
|
||||
if (!row) {
|
||||
throw new Error('Service attribute row not found');
|
||||
}
|
||||
return row;
|
||||
});
|
||||
|
||||
const filterButtons = serviceRow.querySelectorAll('button.filter-btn');
|
||||
expect(filterButtons?.length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// the second button that represents filter out
|
||||
await userEvent.click(filterButtons[1] as HTMLButtonElement);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Log details')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const matched = requestPayloads.some((payload) => {
|
||||
const expression =
|
||||
payload.compositeQuery?.queries?.[0]?.spec?.filter?.expression || '';
|
||||
return (
|
||||
(expression.includes('attributes_string.service') ||
|
||||
expression.includes('service')) &&
|
||||
expression.includes("('frontend')") &&
|
||||
(expression.includes('NIN') || expression.includes('NOT_IN'))
|
||||
);
|
||||
});
|
||||
expect(matched).toBe(true);
|
||||
},
|
||||
{ timeout: 2500 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('time range change', () => {
|
||||
it('should use different time ranges for different renders', async () => {
|
||||
const requestPayloads: any[] = [];
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, async (req, res, ctx) => {
|
||||
const payload = await req.json();
|
||||
requestPayloads.push(payload);
|
||||
return res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 })));
|
||||
}),
|
||||
);
|
||||
|
||||
// First render with initial time range
|
||||
const { unmount } = renderComponent();
|
||||
|
||||
// Wait for initial fetch
|
||||
await waitFor(() => {
|
||||
expect(requestPayloads.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
const firstStartTime = requestPayloads[0].start;
|
||||
expect(firstStartTime).toBe(defaultProps.timeRange.startTime * 1000);
|
||||
|
||||
// Unmount and render again with different time range
|
||||
unmount();
|
||||
|
||||
const newTimeRange = {
|
||||
startTime: 1709000000,
|
||||
endTime: 1709003600,
|
||||
};
|
||||
|
||||
renderComponent({
|
||||
...defaultProps,
|
||||
timeRange: newTimeRange,
|
||||
});
|
||||
|
||||
// Wait for fetch with new time range
|
||||
await waitFor(() => {
|
||||
const hasNewTimeRange = requestPayloads.some(
|
||||
(p) => p.start === newTimeRange.startTime * 1000,
|
||||
);
|
||||
expect(hasNewTimeRange).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call handleTimeChange callback when time picker is clicked', async () => {
|
||||
const mockHandleTimeChange = jest.fn();
|
||||
|
||||
server.use(
|
||||
rest.post(QUERY_RANGE_URL, (_, res, ctx) =>
|
||||
res(ctx.status(200), ctx.json(createLogsResponse({ pageSize: 5 }))),
|
||||
),
|
||||
);
|
||||
|
||||
renderComponent({
|
||||
...defaultProps,
|
||||
handleTimeChange: mockHandleTimeChange,
|
||||
});
|
||||
|
||||
// Wait for component to render
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('time-picker-btn')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click the time picker button (from mock)
|
||||
await userEvent.click(screen.getByTestId('time-picker-btn'));
|
||||
|
||||
// Verify the callback was called
|
||||
expect(mockHandleTimeChange).toHaveBeenCalledWith('5m');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,240 @@
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import {
|
||||
mockQueryRangeV5WithError,
|
||||
mockQueryRangeV5WithLogsResponse,
|
||||
} from '__tests__/query_range_v5.utility';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
|
||||
import { useInfiniteHostMetricLogs } from '../hooks';
|
||||
|
||||
const createWrapper = (): React.FC<{ children: React.ReactNode }> => {
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return function Wrapper({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
describe('useInfiniteHostMetricLogs', () => {
|
||||
const defaultParams = {
|
||||
expression: 'host_name = "test-host"',
|
||||
startTime: 1708000000,
|
||||
endTime: 1708003600,
|
||||
};
|
||||
|
||||
describe('initial state', () => {
|
||||
it('should return initial loading state', () => {
|
||||
mockQueryRangeV5WithLogsResponse({
|
||||
delay: 100,
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInfiniteHostMetricLogs(defaultParams),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.isLoading).toBe(true);
|
||||
expect(result.current.logs).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('successful data fetching', () => {
|
||||
it('should return logs after successful fetch', async () => {
|
||||
mockQueryRangeV5WithLogsResponse({
|
||||
pageSize: 5,
|
||||
hasMore: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInfiniteHostMetricLogs(defaultParams),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.error).toBeFalsy();
|
||||
expect(result.current.logs.length).toBe(5);
|
||||
expect(result.current.isError).toBe(false);
|
||||
});
|
||||
|
||||
it('should set hasNextPage based on response size', async () => {
|
||||
mockQueryRangeV5WithLogsResponse({
|
||||
pageSize: 100,
|
||||
hasMore: true,
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInfiniteHostMetricLogs(defaultParams),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.hasNextPage).toBe(true);
|
||||
});
|
||||
|
||||
it('should not have next page when response is smaller than page size', async () => {
|
||||
mockQueryRangeV5WithLogsResponse({
|
||||
pageSize: 100,
|
||||
hasMore: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInfiniteHostMetricLogs(defaultParams),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.hasNextPage).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty state', () => {
|
||||
it('should return empty logs array when no data', async () => {
|
||||
mockQueryRangeV5WithLogsResponse({
|
||||
pageSize: 0,
|
||||
hasMore: false,
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInfiniteHostMetricLogs(defaultParams),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.logs).toEqual([]);
|
||||
expect(result.current.hasNextPage).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should set isError on API failure', async () => {
|
||||
mockQueryRangeV5WithError('Internal Server Error');
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInfiniteHostMetricLogs(defaultParams),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isError).toBe(true);
|
||||
});
|
||||
|
||||
expect(result.current.logs).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('query disabled state', () => {
|
||||
it('should not fetch when expression is empty', async () => {
|
||||
const requestCount = { count: 0 };
|
||||
|
||||
mockQueryRangeV5WithLogsResponse({
|
||||
pageSize: 0,
|
||||
hasMore: false,
|
||||
onReceiveRequest: (): void => {
|
||||
requestCount.count += 1;
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useInfiniteHostMetricLogs({
|
||||
...defaultParams,
|
||||
expression: '',
|
||||
}),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
// Wait a bit to ensure no request is made
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 300);
|
||||
});
|
||||
|
||||
expect(requestCount.count).toBe(0);
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('load more functionality', () => {
|
||||
it('should fetch next page when loadMoreLogs is called', async () => {
|
||||
const requestCount = { count: 0 };
|
||||
|
||||
mockQueryRangeV5WithLogsResponse({
|
||||
pageSize: 100,
|
||||
offset: 0,
|
||||
hasMore: true,
|
||||
onReceiveRequest: () => {
|
||||
requestCount.count += 1;
|
||||
|
||||
if (requestCount.count > 1) {
|
||||
return { offset: 100, pageSize: 100, hasMore: false };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useInfiniteHostMetricLogs(defaultParams),
|
||||
{
|
||||
wrapper: createWrapper(),
|
||||
},
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.logs.length).toBe(100);
|
||||
expect(result.current.hasNextPage).toBe(true);
|
||||
expect(requestCount.count).toBe(1);
|
||||
|
||||
act(() => {
|
||||
result.current.loadMoreLogs();
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.logs.length).toBe(150);
|
||||
});
|
||||
|
||||
expect(result.current.hasNextPage).toBe(false);
|
||||
expect(requestCount.count).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
@@ -6,56 +7,82 @@ import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const getHostLogsQueryPayload = (
|
||||
start: number,
|
||||
end: number,
|
||||
filters: IBuilderQuery['filters'],
|
||||
): GetQueryResultsProps => ({
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
query: {
|
||||
clickhouse_sql: [],
|
||||
promql: [],
|
||||
builder: {
|
||||
queryData: [
|
||||
{
|
||||
dataSource: DataSource.LOGS,
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
id: '------false',
|
||||
dataType: DataTypes.String,
|
||||
key: '',
|
||||
type: '',
|
||||
},
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filters,
|
||||
expression: 'A',
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
having: [],
|
||||
limit: null,
|
||||
orderBy: [
|
||||
{
|
||||
columnName: 'timestamp',
|
||||
order: 'desc',
|
||||
},
|
||||
],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
offset: 0,
|
||||
pageSize: 100,
|
||||
},
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: uuidv4(),
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
export interface HostLogsQueryParams {
|
||||
start: number;
|
||||
end: number;
|
||||
expression: string;
|
||||
offset?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export const getHostLogsQueryPayload = ({
|
||||
start,
|
||||
end,
|
||||
});
|
||||
expression,
|
||||
offset = 0,
|
||||
pageSize = DEFAULT_PER_PAGE_VALUE,
|
||||
}: HostLogsQueryParams): {
|
||||
query: GetQueryResultsProps;
|
||||
queryData: IBuilderQuery;
|
||||
} => {
|
||||
const queryData: IBuilderQuery = {
|
||||
dataSource: DataSource.LOGS,
|
||||
queryName: 'A',
|
||||
aggregateOperator: 'noop',
|
||||
aggregateAttribute: {
|
||||
id: '------false',
|
||||
dataType: DataTypes.String,
|
||||
key: '',
|
||||
type: '',
|
||||
},
|
||||
timeAggregation: 'rate',
|
||||
spaceAggregation: 'sum',
|
||||
functions: [],
|
||||
filter: { expression },
|
||||
expression,
|
||||
having: {
|
||||
expression: '',
|
||||
},
|
||||
disabled: false,
|
||||
stepInterval: 60,
|
||||
limit: null,
|
||||
orderBy: [
|
||||
{
|
||||
columnName: 'timestamp',
|
||||
order: 'desc',
|
||||
},
|
||||
{
|
||||
columnName: 'id',
|
||||
order: 'desc',
|
||||
},
|
||||
],
|
||||
groupBy: [],
|
||||
legend: '',
|
||||
reduceTo: ReduceOperators.AVG,
|
||||
offset,
|
||||
pageSize,
|
||||
};
|
||||
|
||||
return {
|
||||
query: {
|
||||
graphType: PANEL_TYPES.LIST,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
query: {
|
||||
clickhouse_sql: [],
|
||||
promql: [],
|
||||
builder: {
|
||||
queryData: [queryData],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
id: uuidv4(),
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
},
|
||||
start,
|
||||
end,
|
||||
},
|
||||
queryData,
|
||||
};
|
||||
};
|
||||
|
||||
export const HOST_METRICS_LOGS_EXPR_QUERY_KEY = 'hostMetricsLogsExpr';
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useInfiniteQuery } from 'react-query';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { DEFAULT_PER_PAGE_VALUE } from 'container/Controls/config';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { ILog } from 'types/api/logs/log';
|
||||
|
||||
import { getHostLogsQueryPayload } from './constants';
|
||||
|
||||
export function useInfiniteHostMetricLogs({
|
||||
expression,
|
||||
startTime,
|
||||
endTime,
|
||||
}: {
|
||||
expression: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
}): {
|
||||
logs: ILog[];
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
isFetchingNextPage: boolean;
|
||||
isError: boolean;
|
||||
error?: unknown;
|
||||
hasNextPage: boolean;
|
||||
loadMoreLogs: () => void;
|
||||
} {
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
isError,
|
||||
error,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
} = useInfiniteQuery({
|
||||
queryKey: ['hostMetricsLogs', startTime, endTime, expression],
|
||||
queryFn: async ({ pageParam = 0 }) => {
|
||||
const { query } = getHostLogsQueryPayload({
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
expression,
|
||||
offset: pageParam,
|
||||
pageSize: DEFAULT_PER_PAGE_VALUE,
|
||||
});
|
||||
return GetMetricQueryRange(query, ENTITY_VERSION_V5);
|
||||
},
|
||||
getNextPageParam: (lastPage, allPages) => {
|
||||
const list = lastPage?.payload?.data?.newResult?.data?.result?.[0]?.list;
|
||||
if (!list || list.length < DEFAULT_PER_PAGE_VALUE) {
|
||||
return undefined;
|
||||
}
|
||||
return allPages.length * DEFAULT_PER_PAGE_VALUE;
|
||||
},
|
||||
enabled: !!expression,
|
||||
});
|
||||
|
||||
const logs = useMemo<ILog[]>(() => {
|
||||
if (!data?.pages) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.pages.flatMap((page) => {
|
||||
const list = page.payload.data.newResult.data.result?.[0]?.list;
|
||||
if (!list) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return list.map(
|
||||
(item) =>
|
||||
({
|
||||
...item.data,
|
||||
timestamp: item.timestamp,
|
||||
} as ILog),
|
||||
);
|
||||
});
|
||||
}, [data?.pages]);
|
||||
|
||||
const loadMoreLogs = useCallback(() => {
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
||||
|
||||
return {
|
||||
logs,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isFetchingNextPage,
|
||||
isError,
|
||||
error,
|
||||
hasNextPage: !!hasNextPage,
|
||||
loadMoreLogs,
|
||||
};
|
||||
}
|
||||
@@ -202,19 +202,8 @@ function InviteMembersModal({
|
||||
onComplete?.();
|
||||
} catch (err) {
|
||||
const apiErr = err as APIError;
|
||||
if (apiErr?.getHttpStatusCode() === 409) {
|
||||
toast.error(
|
||||
touchedRows.length === 1
|
||||
? `${touchedRows[0].email} is already a member`
|
||||
: 'Invite for one or more users already exists',
|
||||
{ richColors: true },
|
||||
);
|
||||
} else {
|
||||
const errorMessage = apiErr?.getErrorMessage?.() ?? 'An error occurred';
|
||||
toast.error(`Failed to send invites: ${errorMessage}`, {
|
||||
richColors: true,
|
||||
});
|
||||
}
|
||||
const errorMessage = apiErr?.getErrorMessage?.() ?? 'An error occurred';
|
||||
toast.error(errorMessage, { richColors: true });
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import inviteUsers from 'api/v1/invite/bulk/create';
|
||||
import sendInvite from 'api/v1/invite/create';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import InviteMembersModal from '../InviteMembersModal';
|
||||
|
||||
const makeApiError = (message: string, code = StatusCodes.CONFLICT): APIError =>
|
||||
new APIError({
|
||||
httpStatusCode: code,
|
||||
error: { code: 'already_exists', message, url: '', errors: [] },
|
||||
});
|
||||
|
||||
jest.mock('api/v1/invite/create');
|
||||
jest.mock('api/v1/invite/bulk/create');
|
||||
jest.mock('@signozhq/sonner', () => ({
|
||||
@@ -142,6 +151,90 @@ describe('InviteMembersModal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('shows BE message on single invite 409', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
mockSendInvite.mockRejectedValue(
|
||||
makeApiError('An invite already exists for this email: single@signoz.io'),
|
||||
);
|
||||
|
||||
render(<InviteMembersModal {...defaultProps} />);
|
||||
|
||||
await user.type(
|
||||
screen.getAllByPlaceholderText('john@signoz.io')[0],
|
||||
'single@signoz.io',
|
||||
);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /invite team members/i }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
'An invite already exists for this email: single@signoz.io',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows BE message on bulk invite 409', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
mockInviteUsers.mockRejectedValue(
|
||||
makeApiError('An invite already exists for this email: alice@signoz.io'),
|
||||
);
|
||||
|
||||
render(<InviteMembersModal {...defaultProps} />);
|
||||
|
||||
const emailInputs = screen.getAllByPlaceholderText('john@signoz.io');
|
||||
await user.type(emailInputs[0], 'alice@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
|
||||
await user.type(emailInputs[1], 'bob@signoz.io');
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
const editorOptions = await screen.findAllByText('Editor');
|
||||
await user.click(editorOptions[editorOptions.length - 1]);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /invite team members/i }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
'An invite already exists for this email: alice@signoz.io',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows BE message on generic error', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
mockSendInvite.mockRejectedValue(
|
||||
makeApiError('Internal server error', StatusCodes.INTERNAL_SERVER_ERROR),
|
||||
);
|
||||
|
||||
render(<InviteMembersModal {...defaultProps} />);
|
||||
|
||||
await user.type(
|
||||
screen.getAllByPlaceholderText('john@signoz.io')[0],
|
||||
'single@signoz.io',
|
||||
);
|
||||
await user.click(screen.getAllByText('Select roles')[0]);
|
||||
await user.click(await screen.findByText('Viewer'));
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /invite team members/i }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
'Internal server error',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('uses inviteUsers (bulk) when multiple rows are filled', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const onComplete = jest.fn();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
|
||||
import { useCopyToClipboard, useLocation } from 'react-use';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { Button, Divider, Drawer, Radio, Tooltip, Typography } from 'antd';
|
||||
@@ -49,6 +49,7 @@ import { AppState } from 'store/reducers';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, StringOperators } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import { RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
|
||||
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
|
||||
@@ -221,7 +222,7 @@ function LogDetailInner({
|
||||
};
|
||||
|
||||
// Go to logs explorer page with the log data
|
||||
const handleOpenInExplorer = (): void => {
|
||||
const handleOpenInExplorer = (e?: React.MouseEvent): void => {
|
||||
const queryParams = {
|
||||
[QueryParams.activeLogId]: `"${log?.id}"`,
|
||||
[QueryParams.startTime]: minTime?.toString() || '',
|
||||
@@ -234,7 +235,9 @@ function LogDetailInner({
|
||||
),
|
||||
),
|
||||
};
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`);
|
||||
safeNavigate(`${ROUTES.LOGS_EXPLORER}?${createQueryParams(queryParams)}`, {
|
||||
newTab: !!e && isModifierKeyPressed(e),
|
||||
});
|
||||
};
|
||||
|
||||
const handleQueryExpressionChange = useCallback(
|
||||
|
||||
@@ -17,6 +17,7 @@ function CodeCopyBtn({
|
||||
let copiedText = '';
|
||||
if (children && Array.isArray(children)) {
|
||||
setIsSnippetCopied(true);
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
navigator.clipboard.writeText(children[0].props.children[0]).finally(() => {
|
||||
copiedText = (children[0].props.children[0] as string).slice(0, 200); // slicing is done due to the limitation in accepted char length in attributes
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -401,6 +401,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
|
||||
|
||||
const textToCopy = selectedTexts.join(', ');
|
||||
|
||||
// eslint-disable-next-line no-restricted-properties
|
||||
navigator.clipboard.writeText(textToCopy).catch(console.error);
|
||||
}, [selectedChips, selectedValues]);
|
||||
|
||||
|
||||
@@ -86,8 +86,8 @@ jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: (): boolean => false,
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): { selectedDashboard: undefined } => ({
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): { selectedDashboard: undefined } => ({
|
||||
selectedDashboard: undefined,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { DialogWrapper } from '@signozhq/dialog';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
@@ -105,19 +106,23 @@ function AddKeyModal(): JSX.Element {
|
||||
});
|
||||
}
|
||||
|
||||
const [copyState, copyToClipboard] = useCopyToClipboard();
|
||||
const handleCopy = useCallback(async (): Promise<void> => {
|
||||
if (!createdKey?.key) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(createdKey.key);
|
||||
setHasCopied(true);
|
||||
setTimeout(() => setHasCopied(false), 2000);
|
||||
toast.success('Key copied to clipboard', { richColors: true });
|
||||
} catch {
|
||||
|
||||
copyToClipboard(createdKey.key);
|
||||
setHasCopied(true);
|
||||
setTimeout(() => setHasCopied(false), 2000);
|
||||
toast.success('Key copied to clipboard', { richColors: true });
|
||||
}, [copyToClipboard, createdKey?.key]);
|
||||
|
||||
useEffect(() => {
|
||||
if (copyState.error) {
|
||||
toast.error('Failed to copy key', { richColors: true });
|
||||
}
|
||||
}, [createdKey]);
|
||||
}, [copyState.error]);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
setIsAddKeyOpen(null);
|
||||
|
||||
@@ -9,6 +9,16 @@ jest.mock('@signozhq/sonner', () => ({
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
const mockCopyToClipboard = jest.fn();
|
||||
const mockCopyState = { value: undefined, error: undefined };
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
useCopyToClipboard: (): [typeof mockCopyState, typeof mockCopyToClipboard] => [
|
||||
mockCopyState,
|
||||
mockCopyToClipboard,
|
||||
],
|
||||
}));
|
||||
|
||||
const mockToast = jest.mocked(toast);
|
||||
|
||||
const SA_KEYS_ENDPOINT = '*/api/v1/service_accounts/sa-1/keys';
|
||||
@@ -35,16 +45,9 @@ function renderModal(): ReturnType<typeof render> {
|
||||
}
|
||||
|
||||
describe('AddKeyModal', () => {
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: { writeText: jest.fn().mockResolvedValue(undefined) },
|
||||
configurable: true,
|
||||
writable: true,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockCopyToClipboard.mockClear();
|
||||
server.use(
|
||||
rest.post(SA_KEYS_ENDPOINT, (_, res, ctx) =>
|
||||
res(ctx.status(201), ctx.json(createdKeyResponse)),
|
||||
@@ -90,9 +93,6 @@ describe('AddKeyModal', () => {
|
||||
|
||||
it('copy button writes key to clipboard and shows toast.success', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
const writeTextSpy = jest
|
||||
.spyOn(navigator.clipboard, 'writeText')
|
||||
.mockResolvedValue(undefined);
|
||||
|
||||
renderModal();
|
||||
|
||||
@@ -115,14 +115,12 @@ describe('AddKeyModal', () => {
|
||||
await user.click(copyBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(writeTextSpy).toHaveBeenCalledWith('snz_abc123xyz456secret');
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('snz_abc123xyz456secret');
|
||||
expect(mockToast.success).toHaveBeenCalledWith(
|
||||
'Key copied to clipboard',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
writeTextSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('Cancel button closes the modal', async () => {
|
||||
|
||||
@@ -16,9 +16,9 @@ function AverageResolutionCard({
|
||||
}: TotalTriggeredCardProps): JSX.Element {
|
||||
return (
|
||||
<StatsCard
|
||||
displayValue={formatTime(currentAvgResolutionTime)}
|
||||
totalCurrentCount={currentAvgResolutionTime}
|
||||
totalPastCount={pastAvgResolutionTime}
|
||||
displayValue={formatTime(+currentAvgResolutionTime)}
|
||||
totalCurrentCount={+currentAvgResolutionTime}
|
||||
totalPastCount={+pastAvgResolutionTime}
|
||||
title="Avg. Resolution Time"
|
||||
timeSeries={timeSeries}
|
||||
/>
|
||||
|
||||
@@ -800,14 +800,10 @@
|
||||
|
||||
.ant-table-cell:has(.top-services-item-latency) {
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
background: rgba(171, 189, 255, 0.04);
|
||||
}
|
||||
|
||||
.ant-table-cell:has(.top-services-item-latency-title) {
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
background: rgba(171, 189, 255, 0.04);
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
.api-quick-filters-header {
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
border-right: 1px solid var(--bg-slate-400);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -26,6 +25,7 @@
|
||||
width: 100%;
|
||||
|
||||
.toolbar {
|
||||
border-top: 0px;
|
||||
border-bottom: 1px solid var(--bg-slate-400);
|
||||
}
|
||||
|
||||
@@ -220,6 +220,18 @@
|
||||
}
|
||||
|
||||
.lightMode {
|
||||
.api-quick-filter-left-section {
|
||||
.api-quick-filters-header {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.api-module-right-section {
|
||||
.toolbar {
|
||||
border-bottom: 1px solid var(--bg-vanilla-300);
|
||||
}
|
||||
}
|
||||
|
||||
.no-filtered-domains-message-container {
|
||||
.no-filtered-domains-message-content {
|
||||
.no-filtered-domains-message {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
|
||||
import { FeatureKeys } from 'constants/features';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { AlertTypes } from 'types/api/alerts/alertTypes';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import { getOptionList } from './config';
|
||||
import { AlertTypeCard, SelectTypeContainer } from './styles';
|
||||
@@ -70,8 +71,8 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
</Tag>
|
||||
) : undefined
|
||||
}
|
||||
onClick={(): void => {
|
||||
onSelect(option.selection);
|
||||
onClick={(e): void => {
|
||||
onSelect(option.selection, isModifierKeyPressed(e));
|
||||
}}
|
||||
data-testid={`alert-type-card-${option.selection}`}
|
||||
>
|
||||
@@ -108,7 +109,7 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
|
||||
}
|
||||
|
||||
interface SelectAlertTypeProps {
|
||||
onSelect: (typ: AlertTypes) => void;
|
||||
onSelect: (type: AlertTypes, newTab?: boolean) => void;
|
||||
}
|
||||
|
||||
export default SelectAlertType;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Tooltip, Typography } from 'antd';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { Check, Loader, Send, X } from 'lucide-react';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import { useCreateAlertState } from '../context';
|
||||
import {
|
||||
@@ -33,9 +34,9 @@ function Footer(): JSX.Element {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
|
||||
const handleDiscard = (): void => {
|
||||
const handleDiscard = (e: React.MouseEvent): void => {
|
||||
discardAlertRule();
|
||||
safeNavigate('/alerts');
|
||||
safeNavigate('/alerts', { newTab: isModifierKeyPressed(e) });
|
||||
};
|
||||
|
||||
const alertValidationMessage = useMemo(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MemoryRouter, useLocation } from 'react-router-dom';
|
||||
import { useDashboardBootstrap } from 'hooks/dashboard/useDashboardBootstrap';
|
||||
import {
|
||||
getDashboardById,
|
||||
getNonIntegrationDashboardById,
|
||||
@@ -6,10 +8,9 @@ import {
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import {
|
||||
DashboardContext,
|
||||
DashboardProvider,
|
||||
} from 'providers/Dashboard/Dashboard';
|
||||
import { IDashboardContext } from 'providers/Dashboard/types';
|
||||
resetDashboard,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
@@ -21,6 +22,18 @@ import { Dashboard } from 'types/api/dashboard/getAll';
|
||||
|
||||
import DashboardDescription from '..';
|
||||
|
||||
function DashboardBootstrapWrapper({
|
||||
dashboardId,
|
||||
children,
|
||||
}: {
|
||||
dashboardId: string;
|
||||
children: ReactNode;
|
||||
}): JSX.Element {
|
||||
useDashboardBootstrap(dashboardId);
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
interface MockSafeNavigateReturn {
|
||||
safeNavigate: jest.MockedFunction<(url: string) => void>;
|
||||
}
|
||||
@@ -54,6 +67,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
beforeEach(() => {
|
||||
mockSafeNavigate.mockClear();
|
||||
sessionStorage.clear();
|
||||
resetDashboard();
|
||||
});
|
||||
|
||||
it('unlock dashboard should be disabled for integrations created dashboards', async () => {
|
||||
@@ -64,7 +78,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
const { getByTestId } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardProvider dashboardId="4">
|
||||
<DashboardBootstrapWrapper dashboardId="4">
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
@@ -73,7 +87,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardProvider>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
@@ -105,7 +119,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
);
|
||||
const { getByTestId } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardProvider dashboardId="4">
|
||||
<DashboardBootstrapWrapper dashboardId="4">
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
@@ -114,7 +128,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardProvider>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
@@ -144,7 +158,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
|
||||
const { getByText } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardProvider dashboardId="4">
|
||||
<DashboardBootstrapWrapper dashboardId="4">
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
@@ -153,7 +167,7 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardProvider>
|
||||
</DashboardBootstrapWrapper>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
@@ -181,37 +195,26 @@ describe('Dashboard landing page actions header tests', () => {
|
||||
|
||||
(useLocation as jest.Mock).mockReturnValue(mockLocation);
|
||||
|
||||
const mockContextValue: IDashboardContext = {
|
||||
isDashboardLocked: false,
|
||||
handleDashboardLockToggle: jest.fn(),
|
||||
dashboardResponse: {} as IDashboardContext['dashboardResponse'],
|
||||
useDashboardStore.setState({
|
||||
selectedDashboard: (getDashboardById.data as unknown) as Dashboard,
|
||||
layouts: [],
|
||||
panelMap: {},
|
||||
setPanelMap: jest.fn(),
|
||||
setLayouts: jest.fn(),
|
||||
setSelectedDashboard: jest.fn(),
|
||||
updatedTimeRef: { current: null },
|
||||
updateLocalStorageDashboardVariables: jest.fn(),
|
||||
dashboardQueryRangeCalled: false,
|
||||
setDashboardQueryRangeCalled: jest.fn(),
|
||||
isDashboardFetching: false,
|
||||
columnWidths: {},
|
||||
setColumnWidths: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const { getByText } = render(
|
||||
<MemoryRouter initialEntries={[DASHBOARD_PATH]}>
|
||||
<DashboardContext.Provider value={mockContextValue}>
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
enter: (): Promise<void> => Promise.resolve(),
|
||||
exit: (): Promise<void> => Promise.resolve(),
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</DashboardContext.Provider>
|
||||
<DashboardDescription
|
||||
handle={{
|
||||
active: false,
|
||||
enter: (): Promise<void> => Promise.resolve(),
|
||||
exit: (): Promise<void> => Promise.resolve(),
|
||||
node: { current: null },
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import { DeleteButton } from 'container/ListOfDashboard/TableComponents/DeleteBu
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta';
|
||||
import { useLockDashboard } from 'hooks/dashboard/useLockDashboard';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
@@ -39,8 +40,11 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
@@ -79,10 +83,11 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
|
||||
setPanelMap,
|
||||
layouts,
|
||||
setLayouts,
|
||||
isDashboardLocked,
|
||||
setSelectedDashboard,
|
||||
handleDashboardLockToggle,
|
||||
} = useDashboard();
|
||||
} = useDashboardStore();
|
||||
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
const handleDashboardLockToggle = useLockDashboard();
|
||||
|
||||
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
Pyramid,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { AppState } from 'store/reducers';
|
||||
import {
|
||||
IDashboardVariable,
|
||||
@@ -239,7 +239,7 @@ function VariableItem({
|
||||
|
||||
const [selectedWidgets, setSelectedWidgets] = useState<string[]>([]);
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
const widgetsByDynamicVariableId = useWidgetsByDynamicVariableId();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { CustomMultiSelect } from 'components/NewSelect';
|
||||
import { PANEL_GROUP_TYPES } from 'constants/queryBuilder';
|
||||
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { WidgetRow, Widgets } from 'types/api/dashboard/getAll';
|
||||
|
||||
export function WidgetSelector({
|
||||
@@ -12,7 +12,7 @@ export function WidgetSelector({
|
||||
selectedWidgets: string[];
|
||||
setSelectedWidgets: (widgets: string[]) => void;
|
||||
}): JSX.Element {
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
|
||||
// Get layout IDs for cross-referencing
|
||||
const layoutIds = new Set(
|
||||
|
||||
@@ -19,8 +19,8 @@ import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { PenLine, Trash2 } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { TVariableMode } from './types';
|
||||
@@ -87,7 +87,7 @@ function VariablesSettings({
|
||||
|
||||
const { t } = useTranslation(['dashboard']);
|
||||
|
||||
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||
const { selectedDashboard, setSelectedDashboard } = useDashboardStore();
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
@@ -5,7 +5,7 @@ import AddTags from 'container/DashboardContainer/DashboardSettings/General/AddT
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { Check, X } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
|
||||
import { Button } from './styles';
|
||||
import { Base64Icons } from './utils';
|
||||
@@ -15,7 +15,7 @@ import './GeneralSettings.styles.scss';
|
||||
const { Option } = Select;
|
||||
|
||||
function GeneralDashboardSettings(): JSX.Element {
|
||||
const { selectedDashboard, setSelectedDashboard } = useDashboard();
|
||||
const { selectedDashboard, setSelectedDashboard } = useDashboardStore();
|
||||
|
||||
const updateDashboardMutation = useUpdateDashboard();
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { toast } from '@signozhq/sonner';
|
||||
import { fireEvent, within } from '@testing-library/react';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import {
|
||||
publishedPublicDashboardMeta,
|
||||
unpublishedPublicDashboardMeta,
|
||||
} from 'mocks-server/__mockdata__/publicDashboard';
|
||||
import { rest, server } from 'mocks-server/server';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
|
||||
import PublicDashboardSetting from '../index';
|
||||
|
||||
// Mock dependencies
|
||||
jest.mock('providers/Dashboard/Dashboard');
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore');
|
||||
jest.mock('react-use', () => ({
|
||||
...jest.requireActual('react-use'),
|
||||
useCopyToClipboard: jest.fn(),
|
||||
@@ -26,14 +27,13 @@ jest.mock('@signozhq/sonner', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const mockUseDashboard = jest.mocked(useDashboard);
|
||||
const mockUseDashboard = jest.mocked(useDashboardStore);
|
||||
const mockUseCopyToClipboard = jest.mocked(useCopyToClipboard);
|
||||
const mockToast = jest.mocked(toast);
|
||||
|
||||
// Test constants
|
||||
const MOCK_DASHBOARD_ID = 'test-dashboard-id';
|
||||
const MOCK_PUBLIC_PATH = '/public/dashboard/test-dashboard-id';
|
||||
const DEFAULT_TIME_RANGE = '30m';
|
||||
const DASHBOARD_VARIABLES_WARNING =
|
||||
"Dashboard variables won't work in public dashboards";
|
||||
|
||||
@@ -67,10 +67,10 @@ beforeEach(() => {
|
||||
// Mock window.open
|
||||
window.open = jest.fn();
|
||||
|
||||
// Mock useDashboard
|
||||
// Mock useDashboardStore
|
||||
mockUseDashboard.mockReturnValue(({
|
||||
selectedDashboard: mockSelectedDashboard,
|
||||
} as unknown) as ReturnType<typeof useDashboard>);
|
||||
} as unknown) as ReturnType<typeof useDashboardStore>);
|
||||
|
||||
// Mock useCopyToClipboard
|
||||
mockUseCopyToClipboard.mockReturnValue(([
|
||||
|
||||
@@ -7,11 +7,12 @@ import { Button, Select, Typography } from 'antd';
|
||||
import createPublicDashboardAPI from 'api/dashboard/public/createPublicDashboard';
|
||||
import revokePublicDashboardAccessAPI from 'api/dashboard/public/revokePublicDashboardAccess';
|
||||
import updatePublicDashboardAPI from 'api/dashboard/public/updatePublicDashboard';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { useGetPublicDashboardMeta } from 'hooks/dashboard/useGetPublicDashboardMeta';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { Copy, ExternalLink, Globe, Info, Loader2, Trash } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { PublicDashboardMetaProps } from 'types/api/dashboard/public/getMeta';
|
||||
import APIError from 'types/api/error';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
@@ -56,10 +57,10 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
PublicDashboardMetaProps | undefined
|
||||
>(undefined);
|
||||
const [timeRangeEnabled, setTimeRangeEnabled] = useState(true);
|
||||
const [defaultTimeRange, setDefaultTimeRange] = useState('30m');
|
||||
const [defaultTimeRange, setDefaultTimeRange] = useState(DEFAULT_TIME_RANGE);
|
||||
const [, setCopyPublicDashboardURL] = useCopyToClipboard();
|
||||
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
|
||||
const { isCloudUser, isEnterpriseSelfHostedUser } = useGetTenantLicense();
|
||||
|
||||
@@ -99,7 +100,7 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
console.error('Error getting public dashboard', errorPublicDashboard);
|
||||
setPublicDashboardData(undefined);
|
||||
setTimeRangeEnabled(true);
|
||||
setDefaultTimeRange('30m');
|
||||
setDefaultTimeRange(DEFAULT_TIME_RANGE);
|
||||
}
|
||||
}, [publicDashboardResponse, errorPublicDashboard]);
|
||||
|
||||
@@ -109,7 +110,7 @@ function PublicDashboardSetting(): JSX.Element {
|
||||
publicDashboardResponse?.data?.timeRangeEnabled || false,
|
||||
);
|
||||
setDefaultTimeRange(
|
||||
publicDashboardResponse?.data?.defaultTimeRange || '30m',
|
||||
publicDashboardResponse?.data?.defaultTimeRange || DEFAULT_TIME_RANGE,
|
||||
);
|
||||
}
|
||||
}, [publicDashboardResponse]);
|
||||
|
||||
@@ -3,13 +3,14 @@ import { memo, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Row } from 'antd';
|
||||
import { ALL_SELECTED_VALUE } from 'components/NewSelect/utils';
|
||||
import { updateLocalStorageDashboardVariable } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import {
|
||||
useDashboardVariables,
|
||||
useDashboardVariablesSelector,
|
||||
} from 'hooks/dashboard/useDashboardVariables';
|
||||
import useVariablesFromUrl from 'hooks/dashboard/useVariablesFromUrl';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { updateDashboardVariablesStore } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStore';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import {
|
||||
enqueueDescendantsOfVariable,
|
||||
enqueueFetchOfAllVariables,
|
||||
@@ -18,23 +19,23 @@ import {
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import VariableItem from './VariableItem';
|
||||
|
||||
import './DashboardVariableSelection.styles.scss';
|
||||
|
||||
function DashboardVariableSelection(): JSX.Element | null {
|
||||
const {
|
||||
setSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables,
|
||||
} = useDashboard();
|
||||
const { dashboardId, setSelectedDashboard } = useDashboardStore(
|
||||
useShallow((s) => ({
|
||||
dashboardId: s.selectedDashboard?.id ?? '',
|
||||
setSelectedDashboard: s.setSelectedDashboard,
|
||||
})),
|
||||
);
|
||||
|
||||
const { updateUrlVariable } = useVariablesFromUrl();
|
||||
|
||||
const { dashboardVariables } = useDashboardVariables();
|
||||
const dashboardId = useDashboardVariablesSelector(
|
||||
(state) => state.dashboardId,
|
||||
);
|
||||
const sortedVariablesArray = useDashboardVariablesSelector(
|
||||
(state) => state.sortedVariablesArray,
|
||||
);
|
||||
@@ -82,7 +83,13 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
// This makes localStorage much lighter by avoiding storing all individual values
|
||||
const variable = dashboardVariables[id] || dashboardVariables[name];
|
||||
const isDynamic = variable.type === 'DYNAMIC';
|
||||
updateLocalStorageDashboardVariables(name, value, allSelected, isDynamic);
|
||||
updateLocalStorageDashboardVariable(
|
||||
dashboardId,
|
||||
name,
|
||||
value,
|
||||
allSelected,
|
||||
isDynamic,
|
||||
);
|
||||
|
||||
if (allSelected) {
|
||||
updateUrlVariable(name || id, ALL_SELECTED_VALUE);
|
||||
@@ -150,13 +157,7 @@ function DashboardVariableSelection(): JSX.Element | null {
|
||||
// Safe to call synchronously now that the store already has the updated value.
|
||||
enqueueDescendantsOfVariable(name);
|
||||
},
|
||||
[
|
||||
dashboardId,
|
||||
dashboardVariables,
|
||||
updateLocalStorageDashboardVariables,
|
||||
updateUrlVariable,
|
||||
setSelectedDashboard,
|
||||
],
|
||||
[dashboardId, dashboardVariables, updateUrlVariable, setSelectedDashboard],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -32,11 +32,22 @@ const mockVariableItemCallbacks: {
|
||||
// Mock providers/Dashboard/Dashboard
|
||||
const mockSetSelectedDashboard = jest.fn();
|
||||
const mockUpdateLocalStorageDashboardVariables = jest.fn();
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): Record<string, unknown> => ({
|
||||
setSelectedDashboard: mockSetSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables: mockUpdateLocalStorageDashboardVariables,
|
||||
}),
|
||||
interface MockDashboardStoreState {
|
||||
selectedDashboard?: { id: string };
|
||||
setSelectedDashboard: typeof mockSetSelectedDashboard;
|
||||
updateLocalStorageDashboardVariables: typeof mockUpdateLocalStorageDashboardVariables;
|
||||
}
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (
|
||||
selector?: (s: Record<string, unknown>) => MockDashboardStoreState,
|
||||
): MockDashboardStoreState => {
|
||||
const state = {
|
||||
selectedDashboard: { id: 'dash-1' },
|
||||
setSelectedDashboard: mockSetSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables: mockUpdateLocalStorageDashboardVariables,
|
||||
};
|
||||
return selector ? selector(state) : state;
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock hooks/dashboard/useVariablesFromUrl
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useCallback } from 'react';
|
||||
import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels';
|
||||
import { updateLocalStorageDashboardVariable } from 'hooks/dashboard/useDashboardFromLocalStorage';
|
||||
import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { IDashboardVariables } from 'providers/Dashboard/store/dashboardVariables/dashboardVariablesStoreTypes';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { IDashboardVariable } from 'types/api/dashboard/getAll';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { convertVariablesToDbFormat } from './util';
|
||||
|
||||
@@ -37,11 +39,16 @@ interface UseDashboardVariableUpdateReturn {
|
||||
|
||||
export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn => {
|
||||
const {
|
||||
dashboardId,
|
||||
selectedDashboard,
|
||||
setSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables,
|
||||
} = useDashboard();
|
||||
|
||||
} = useDashboardStore(
|
||||
useShallow((s) => ({
|
||||
dashboardId: s.selectedDashboard?.id ?? '',
|
||||
selectedDashboard: s.selectedDashboard,
|
||||
setSelectedDashboard: s.setSelectedDashboard,
|
||||
})),
|
||||
);
|
||||
const addDynamicVariableToPanels = useAddDynamicVariableToPanels();
|
||||
const updateMutation = useUpdateDashboard();
|
||||
|
||||
@@ -59,7 +66,13 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
|
||||
// This makes localStorage much lighter and more efficient.
|
||||
// currently all the variables are dynamic
|
||||
const isDynamic = true;
|
||||
updateLocalStorageDashboardVariables(name, value, allSelected, isDynamic);
|
||||
updateLocalStorageDashboardVariable(
|
||||
dashboardId,
|
||||
name,
|
||||
value,
|
||||
allSelected,
|
||||
isDynamic,
|
||||
);
|
||||
|
||||
if (selectedDashboard) {
|
||||
setSelectedDashboard((prev) => {
|
||||
@@ -97,11 +110,7 @@ export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn =
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
selectedDashboard,
|
||||
setSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables,
|
||||
],
|
||||
[dashboardId, selectedDashboard, setSelectedDashboard],
|
||||
);
|
||||
|
||||
const updateVariables = useCallback(
|
||||
|
||||
@@ -49,8 +49,8 @@ const mockDashboard = {
|
||||
// Mock the dashboard provider with stable functions to prevent infinite loops
|
||||
const mockSetSelectedDashboard = jest.fn();
|
||||
const mockUpdateLocalStorageDashboardVariables = jest.fn();
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): any => ({
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): any => ({
|
||||
selectedDashboard: mockDashboard,
|
||||
setSelectedDashboard: mockSetSelectedDashboard,
|
||||
updateLocalStorageDashboardVariables: mockUpdateLocalStorageDashboardVariables,
|
||||
|
||||
@@ -56,8 +56,8 @@ const mockDashboard = {
|
||||
},
|
||||
};
|
||||
// Mock dependencies
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): any => ({
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): any => ({
|
||||
selectedDashboard: mockDashboard,
|
||||
}),
|
||||
}));
|
||||
@@ -152,8 +152,8 @@ describe('Panel Management Tests', () => {
|
||||
};
|
||||
|
||||
// Temporarily mock the dashboard
|
||||
jest.doMock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): any => ({
|
||||
jest.doMock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): any => ({
|
||||
selectedDashboard: modifiedDashboard,
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -4,7 +4,7 @@ import ROUTES from 'constants/routes';
|
||||
import { DASHBOARDS_LIST_QUERY_PARAMS_STORAGE_KEY } from 'hooks/dashboard/useDashboardsListQueryParams';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { DashboardData } from 'types/api/dashboard/getAll';
|
||||
|
||||
import { Base64Icons } from '../../DashboardSettings/General/utils';
|
||||
@@ -13,7 +13,7 @@ import './DashboardBreadcrumbs.styles.scss';
|
||||
|
||||
function DashboardBreadcrumbs(): JSX.Element {
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
const updatedAtRef = useRef(selectedDashboard?.updatedAt);
|
||||
|
||||
const selectedData = selectedDashboard
|
||||
|
||||
@@ -6,7 +6,10 @@ import { useNotifications } from 'hooks/useNotifications';
|
||||
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
|
||||
import { usePlotContext } from 'lib/uPlotV2/context/PlotContext';
|
||||
import useLegendsSync from 'lib/uPlotV2/hooks/useLegendsSync';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
|
||||
import { getChartManagerColumns } from './getChartMangerColumns';
|
||||
import { ExtendedChartDataset, getDefaultTableDataSet } from './utils';
|
||||
@@ -50,7 +53,7 @@ export default function ChartManager({
|
||||
onToggleSeriesVisibility,
|
||||
syncSeriesVisibilityToLocalStorage,
|
||||
} = usePlotContext();
|
||||
const { isDashboardLocked } = useDashboard();
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
|
||||
const [tableDataSet, setTableDataSet] = useState<ExtendedChartDataset[]>(() =>
|
||||
getDefaultTableDataSet(
|
||||
|
||||
@@ -32,10 +32,18 @@ jest.mock('lib/uPlotV2/hooks/useLegendsSync', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): { isDashboardLocked: boolean } => ({
|
||||
isDashboardLocked: false,
|
||||
}),
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (
|
||||
selector?: (s: {
|
||||
selectedDashboard: { locked: boolean } | undefined;
|
||||
}) => { selectedDashboard: { locked: boolean } },
|
||||
): { selectedDashboard: { locked: boolean } } => {
|
||||
const mockState = { selectedDashboard: { locked: false } };
|
||||
return selector ? selector(mockState) : mockState;
|
||||
},
|
||||
selectIsDashboardLocked: (s: {
|
||||
selectedDashboard: { locked: boolean } | undefined;
|
||||
}): boolean => s.selectedDashboard?.locked ?? false,
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useNotifications', () => ({
|
||||
|
||||
@@ -16,6 +16,8 @@ import { isUndefined } from 'lodash-es';
|
||||
import { urlKey } from 'pages/ErrorDetails/utils';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import { PayloadProps as GetByErrorTypeAndServicePayload } from 'types/api/errors/getByErrorTypeAndService';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { keyToExclude } from './config';
|
||||
import { DashedContainer, EditorContainer, EventContainer } from './styles';
|
||||
@@ -111,14 +113,19 @@ function ErrorDetails(props: ErrorDetailsProps): JSX.Element {
|
||||
value: errorDetail[key as keyof GetByErrorTypeAndServicePayload],
|
||||
}));
|
||||
|
||||
const onClickTraceHandler = (): void => {
|
||||
const onClickTraceHandler = (event: React.MouseEvent): void => {
|
||||
logEvent('Exception: Navigate to trace detail page', {
|
||||
groupId: errorDetail?.groupID,
|
||||
spanId: errorDetail.spanID,
|
||||
traceId: errorDetail.traceID,
|
||||
exceptionId: errorDetail?.errorId,
|
||||
});
|
||||
history.push(`/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`);
|
||||
const path = `/trace/${errorDetail.traceID}?spanId=${errorDetail.spanID}`;
|
||||
if (isModifierKeyPressed(event)) {
|
||||
openInNewTab(path);
|
||||
} else {
|
||||
history.push(path);
|
||||
}
|
||||
};
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useQueryClient } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -44,6 +44,7 @@ import { QueryFunction } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { compositeQueryToQueryEnvelope } from 'utils/compositeQueryToQueryEnvelope';
|
||||
|
||||
import BasicInfo from './BasicInfo';
|
||||
@@ -330,13 +331,18 @@ function FormAlertRules({
|
||||
}
|
||||
}, [alertDef, currentQuery?.queryType, queryOptions]);
|
||||
|
||||
const onCancelHandler = useCallback(() => {
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`);
|
||||
}, [safeNavigate, urlQuery]);
|
||||
const onCancelHandler = useCallback(
|
||||
(e?: React.MouseEvent) => {
|
||||
urlQuery.delete(QueryParams.compositeQuery);
|
||||
urlQuery.delete(QueryParams.panelTypes);
|
||||
urlQuery.delete(QueryParams.ruleId);
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
safeNavigate(`${ROUTES.LIST_ALL_ALERT}?${urlQuery.toString()}`, {
|
||||
newTab: !!e && isModifierKeyPressed(e),
|
||||
});
|
||||
},
|
||||
[safeNavigate, urlQuery],
|
||||
);
|
||||
|
||||
// onQueryCategoryChange handles changes to query category
|
||||
// in state as well as sets additional defaults
|
||||
|
||||
@@ -464,14 +464,10 @@ function GeneralSettings({
|
||||
onModalToggleHandler(type);
|
||||
};
|
||||
|
||||
const {
|
||||
isCloudUser: isCloudUserVal,
|
||||
isEnterpriseSelfHostedUser,
|
||||
} = useGetTenantLicense();
|
||||
const { isCloudUser: isCloudUserVal } = useGetTenantLicense();
|
||||
|
||||
const isAdmin = user.role === USER_ROLES.ADMIN;
|
||||
const showCustomDomainSettings =
|
||||
(isCloudUserVal || isEnterpriseSelfHostedUser) && isAdmin;
|
||||
const showCustomDomainSettings = isCloudUserVal && isAdmin;
|
||||
|
||||
const renderConfig = [
|
||||
{
|
||||
|
||||
@@ -38,7 +38,6 @@ jest.mock('hooks/useComponentPermission', () => ({
|
||||
jest.mock('hooks/useGetTenantLicense', () => ({
|
||||
useGetTenantLicense: jest.fn(() => ({
|
||||
isCloudUser: false,
|
||||
isEnterpriseSelfHostedUser: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -389,7 +388,6 @@ describe('GeneralSettings - S3 Logs Retention', () => {
|
||||
beforeEach(() => {
|
||||
(useGetTenantLicense as jest.Mock).mockReturnValue({
|
||||
isCloudUser: true,
|
||||
isEnterpriseSelfHostedUser: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -411,15 +409,14 @@ describe('GeneralSettings - S3 Logs Retention', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enterprise Self-Hosted User Rendering', () => {
|
||||
describe('Non-cloud user rendering', () => {
|
||||
beforeEach(() => {
|
||||
(useGetTenantLicense as jest.Mock).mockReturnValue({
|
||||
isCloudUser: false,
|
||||
isEnterpriseSelfHostedUser: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render CustomDomainSettings but not GeneralSettingsCloud', () => {
|
||||
it('should not render CustomDomainSettings or GeneralSettingsCloud', () => {
|
||||
render(
|
||||
<GeneralSettings
|
||||
metricsTtlValuesPayload={mockMetricsRetention}
|
||||
@@ -432,12 +429,14 @@ describe('GeneralSettings - S3 Logs Retention', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('custom-domain-settings')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('custom-domain-settings'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId('general-settings-cloud'),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Save buttons should be visible for self-hosted
|
||||
// Save buttons should be visible for non-cloud users (these are from retentions)
|
||||
const saveButtons = screen.getAllByRole('button', { name: /save/i });
|
||||
expect(saveButtons.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
@@ -8,8 +8,11 @@ import { VariablesSettingsTab } from 'container/DashboardContainer/DashboardDesc
|
||||
import DashboardSettings from 'container/DashboardContainer/DashboardSettings';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
|
||||
@@ -20,7 +23,8 @@ export default function DashboardEmptyState(): JSX.Element {
|
||||
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||
);
|
||||
|
||||
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
|
||||
const variablesSettingsTabHandle = useRef<VariablesSettingsTab>(null);
|
||||
const [isSettingsDrawerOpen, setIsSettingsDrawerOpen] = useState<boolean>(
|
||||
|
||||
@@ -3,7 +3,10 @@ import { Button, Input } from 'antd';
|
||||
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
|
||||
import { getGraphManagerTableColumns } from './TableRender/GraphManagerColumns';
|
||||
import { ExtendedChartDataset, GraphManagerProps } from './types';
|
||||
@@ -34,7 +37,7 @@ function GraphManager({
|
||||
}, [data, options]);
|
||||
|
||||
const { notifications } = useNotifications();
|
||||
const { isDashboardLocked } = useDashboard();
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
|
||||
const checkBoxOnChangeHandler = useCallback(
|
||||
(e: CheckboxChangeEvent, index: number): void => {
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
|
||||
import {
|
||||
LoadingOutlined,
|
||||
SearchOutlined,
|
||||
@@ -39,10 +45,14 @@ import { getDashboardVariables } from 'lib/dashboardVariables/getDashboardVariab
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { getGraphType } from 'utils/getGraphType';
|
||||
import { getSortedSeriesData } from 'utils/getSortedSeriesData';
|
||||
|
||||
@@ -81,11 +91,8 @@ function FullView({
|
||||
setCurrentGraphRef(fullViewRef);
|
||||
}, [setCurrentGraphRef]);
|
||||
|
||||
const {
|
||||
selectedDashboard,
|
||||
isDashboardLocked,
|
||||
setColumnWidths,
|
||||
} = useDashboard();
|
||||
const { selectedDashboard, setColumnWidths } = useDashboardStore();
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
|
||||
const onColumnWidthsChange = useCallback(
|
||||
(widths: Record<string, number>) => {
|
||||
@@ -290,9 +297,11 @@ function FullView({
|
||||
<Button
|
||||
className="switch-edit-btn"
|
||||
disabled={response.isFetching || response.isLoading}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
if (dashboardEditView) {
|
||||
safeNavigate(dashboardEditView);
|
||||
safeNavigate(dashboardEditView, {
|
||||
newTab: isModifierKeyPressed(e),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -161,8 +161,8 @@ const mockProps: WidgetGraphComponentProps = {
|
||||
};
|
||||
|
||||
// Mock useDashabord hook
|
||||
jest.mock('providers/Dashboard/Dashboard', () => ({
|
||||
useDashboard: (): any => ({
|
||||
jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
|
||||
useDashboardStore: (): any => ({
|
||||
selectedDashboard: {
|
||||
data: {
|
||||
variables: [],
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
getCustomTimeRangeWindowSweepInMS,
|
||||
getStartAndEndTimesInMilliseconds,
|
||||
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { Props } from 'types/api/dashboard/update';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -106,7 +106,7 @@ function WidgetGraphComponent({
|
||||
selectedDashboard,
|
||||
setSelectedDashboard,
|
||||
setColumnWidths,
|
||||
} = useDashboard();
|
||||
} = useDashboardStore();
|
||||
|
||||
const onColumnWidthsChange = useCallback(
|
||||
(widths: Record<string, number>) => {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { errorDetails } from '../utils';
|
||||
|
||||
function makeAPIError(
|
||||
message: string,
|
||||
code = 'SOME_CODE',
|
||||
errors: { message: string }[] = [],
|
||||
): APIError {
|
||||
return new APIError({
|
||||
httpStatusCode: 500,
|
||||
error: { code, message, url: '', errors },
|
||||
});
|
||||
}
|
||||
|
||||
describe('errorDetails', () => {
|
||||
describe('when passed an APIError', () => {
|
||||
it('returns the error message', () => {
|
||||
const error = makeAPIError('something went wrong');
|
||||
expect(errorDetails(error)).toBe('something went wrong');
|
||||
});
|
||||
|
||||
it('appends details when errors array is non-empty', () => {
|
||||
const error = makeAPIError('query failed', 'QUERY_ERROR', [
|
||||
{ message: 'field X is invalid' },
|
||||
{ message: 'field Y is missing' },
|
||||
]);
|
||||
const result = errorDetails(error);
|
||||
expect(result).toContain('query failed');
|
||||
expect(result).toContain('field X is invalid');
|
||||
expect(result).toContain('field Y is missing');
|
||||
});
|
||||
|
||||
it('does not append details when errors array is empty', () => {
|
||||
const error = makeAPIError('simple error', 'CODE', []);
|
||||
const result = errorDetails(error);
|
||||
expect(result).toBe('simple error');
|
||||
expect(result).not.toContain('Details');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when passed a plain Error (not an APIError)', () => {
|
||||
it('does not throw', () => {
|
||||
const error = new Error('timeout exceeded');
|
||||
expect(() => errorDetails(error)).not.toThrow();
|
||||
});
|
||||
|
||||
it('returns the plain error message', () => {
|
||||
const error = new Error('timeout exceeded');
|
||||
expect(errorDetails(error)).toBe('timeout exceeded');
|
||||
});
|
||||
|
||||
it('returns fallback when plain Error has no message', () => {
|
||||
const error = new Error('');
|
||||
expect(errorDetails(error)).toBe('Unknown error occurred');
|
||||
});
|
||||
});
|
||||
|
||||
describe('fallback behaviour', () => {
|
||||
it('returns "Unknown error occurred" when message is undefined', () => {
|
||||
const error = makeAPIError('');
|
||||
expect(errorDetails(error)).toBe('Unknown error occurred');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { memo, useEffect, useMemo, useRef, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
@@ -17,7 +18,6 @@ import { getVariableReferencesInQuery } from 'lib/dashboardVariables/variableRef
|
||||
import getTimeString from 'lib/getTimeString';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import isEmpty from 'lodash-es/isEmpty';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { AppState } from 'store/reducers';
|
||||
import APIError from 'types/api/error';
|
||||
@@ -68,7 +68,19 @@ function GridCardGraph({
|
||||
const [isInternalServerError, setIsInternalServerError] = useState<boolean>(
|
||||
false,
|
||||
);
|
||||
const { setDashboardQueryRangeCalled } = useDashboard();
|
||||
const queryRangeCalledRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!queryRangeCalledRef.current) {
|
||||
Sentry.captureEvent({
|
||||
message: `Dashboard query range not called within expected timeframe for widget ${widget?.id}`,
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
}, 120000);
|
||||
return (): void => clearTimeout(timeoutId);
|
||||
}, [widget?.id]);
|
||||
|
||||
const {
|
||||
minTime,
|
||||
@@ -260,14 +272,14 @@ function GridCardGraph({
|
||||
});
|
||||
}
|
||||
}
|
||||
setDashboardQueryRangeCalled(true);
|
||||
queryRangeCalledRef.current = true;
|
||||
},
|
||||
onSettled: (data) => {
|
||||
dataAvailable?.(
|
||||
isDataAvailableByPanelType(data?.payload?.data, widget?.panelTypes),
|
||||
);
|
||||
getGraphData?.(data?.payload?.data);
|
||||
setDashboardQueryRangeCalled(true);
|
||||
queryRangeCalledRef.current = true;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -249,13 +249,14 @@ export const handleGraphClick = async ({
|
||||
}
|
||||
};
|
||||
|
||||
export const errorDetails = (error: APIError): string => {
|
||||
const { message, errors } = error.getErrorDetails()?.error || {};
|
||||
export const errorDetails = (error: APIError | Error): string => {
|
||||
const { message, errors } =
|
||||
(error instanceof APIError ? error.getErrorDetails()?.error : null) || {};
|
||||
|
||||
const details =
|
||||
errors?.length > 0
|
||||
errors && errors.length > 0
|
||||
? `\n\nDetails: ${errors.map((e) => e.message).join('\n')}`
|
||||
: '';
|
||||
const errorDetails = `${message} ${details}`;
|
||||
return errorDetails || 'Unknown error occurred';
|
||||
const errorDetails = `${message ?? error.message} ${details}`;
|
||||
return errorDetails.trim() || 'Unknown error occurred';
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { FullScreen, FullScreenHandle } from 'react-full-screen';
|
||||
import { ItemCallback, Layout } from 'react-grid-layout';
|
||||
import { useIsFetching } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Form, Input, Modal, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -12,6 +12,7 @@ import cx from 'classnames';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import { DEFAULT_ROW_NAME } from 'container/DashboardContainer/DashboardDescription/utils';
|
||||
import { useDashboardVariables } from 'hooks/dashboard/useDashboardVariables';
|
||||
@@ -31,7 +32,10 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { sortLayout } from 'providers/Dashboard/util';
|
||||
import { UpdateTimeInterval } from 'store/actions';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
@@ -44,6 +48,7 @@ import DashboardEmptyState from './DashboardEmptyState/DashboardEmptyState';
|
||||
import GridCard from './GridCard';
|
||||
import { Card, CardContainer, ReactGridLayout } from './styles';
|
||||
import {
|
||||
applyRowCollapse,
|
||||
hasColumnWidthsChanged,
|
||||
removeUndefinedValuesFromLayout,
|
||||
} from './utils';
|
||||
@@ -61,6 +66,9 @@ interface GraphLayoutProps {
|
||||
function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
const { handle, enableDrillDown = false } = props;
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const isDashboardFetching =
|
||||
useIsFetching([REACT_QUERY_KEY.DASHBOARD_BY_ID]) > 0;
|
||||
|
||||
const {
|
||||
selectedDashboard,
|
||||
layouts,
|
||||
@@ -68,12 +76,9 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
panelMap,
|
||||
setPanelMap,
|
||||
setSelectedDashboard,
|
||||
isDashboardLocked,
|
||||
dashboardQueryRangeCalled,
|
||||
setDashboardQueryRangeCalled,
|
||||
isDashboardFetching,
|
||||
columnWidths,
|
||||
} = useDashboard();
|
||||
} = useDashboardStore();
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
const { data } = selectedDashboard || {};
|
||||
const { pathname } = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
@@ -137,25 +142,6 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
setDashboardLayout(sortLayout(layouts));
|
||||
}, [layouts]);
|
||||
|
||||
useEffect(() => {
|
||||
setDashboardQueryRangeCalled(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
// Send Sentry event if query_range is not called within expected timeframe (2 mins) when there are widgets
|
||||
if (!dashboardQueryRangeCalled && data?.widgets?.length) {
|
||||
Sentry.captureEvent({
|
||||
message: `Dashboard query range not called within expected timeframe even when there are ${data?.widgets?.length} widgets`,
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
}, 120000);
|
||||
|
||||
return (): void => clearTimeout(timeoutId);
|
||||
}, [dashboardQueryRangeCalled, data?.widgets?.length]);
|
||||
|
||||
const logEventCalledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!logEventCalledRef.current && !isUndefined(data)) {
|
||||
@@ -283,13 +269,10 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
return;
|
||||
}
|
||||
|
||||
currentWidget.title = newTitle;
|
||||
const updatedWidgets = selectedDashboard?.data?.widgets?.filter(
|
||||
(e) => e.id !== currentSelectRowId,
|
||||
const updatedWidgets = selectedDashboard?.data?.widgets?.map((e) =>
|
||||
e.id === currentSelectRowId ? { ...e, title: newTitle } : e,
|
||||
);
|
||||
|
||||
updatedWidgets?.push(currentWidget);
|
||||
|
||||
const updatedSelectedDashboard: Props = {
|
||||
id: selectedDashboard.id,
|
||||
data: {
|
||||
@@ -331,88 +314,13 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element {
|
||||
if (!selectedDashboard) {
|
||||
return;
|
||||
}
|
||||
const rowProperties = { ...currentPanelMap[id] };
|
||||
const updatedPanelMap = { ...currentPanelMap };
|
||||
|
||||
let updatedDashboardLayout = [...dashboardLayout];
|
||||
if (rowProperties.collapsed === true) {
|
||||
rowProperties.collapsed = false;
|
||||
const widgetsInsideTheRow = rowProperties.widgets;
|
||||
let maxY = 0;
|
||||
widgetsInsideTheRow.forEach((w) => {
|
||||
maxY = Math.max(maxY, w.y + w.h);
|
||||
});
|
||||
const currentRowWidget = dashboardLayout.find((w) => w.i === id);
|
||||
if (currentRowWidget && widgetsInsideTheRow.length) {
|
||||
maxY -= currentRowWidget.h + currentRowWidget.y;
|
||||
}
|
||||
|
||||
const idxCurrentRow = dashboardLayout.findIndex((w) => w.i === id);
|
||||
|
||||
for (let j = idxCurrentRow + 1; j < dashboardLayout.length; j++) {
|
||||
updatedDashboardLayout[j].y += maxY;
|
||||
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
|
||||
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
|
||||
updatedDashboardLayout[j].i
|
||||
].widgets.map((w) => ({
|
||||
...w,
|
||||
y: w.y + maxY,
|
||||
}));
|
||||
}
|
||||
}
|
||||
updatedDashboardLayout = [...updatedDashboardLayout, ...widgetsInsideTheRow];
|
||||
} else {
|
||||
rowProperties.collapsed = true;
|
||||
const currentIdx = dashboardLayout.findIndex((w) => w.i === id);
|
||||
|
||||
let widgetsInsideTheRow: Layout[] = [];
|
||||
let isPanelMapUpdated = false;
|
||||
for (let j = currentIdx + 1; j < dashboardLayout.length; j++) {
|
||||
if (currentPanelMap[dashboardLayout[j].i]) {
|
||||
rowProperties.widgets = widgetsInsideTheRow;
|
||||
widgetsInsideTheRow = [];
|
||||
isPanelMapUpdated = true;
|
||||
break;
|
||||
} else {
|
||||
widgetsInsideTheRow.push(dashboardLayout[j]);
|
||||
}
|
||||
}
|
||||
if (!isPanelMapUpdated) {
|
||||
rowProperties.widgets = widgetsInsideTheRow;
|
||||
}
|
||||
let maxY = 0;
|
||||
widgetsInsideTheRow.forEach((w) => {
|
||||
maxY = Math.max(maxY, w.y + w.h);
|
||||
});
|
||||
const currentRowWidget = dashboardLayout[currentIdx];
|
||||
if (currentRowWidget && widgetsInsideTheRow.length) {
|
||||
maxY -= currentRowWidget.h + currentRowWidget.y;
|
||||
}
|
||||
for (let j = currentIdx + 1; j < updatedDashboardLayout.length; j++) {
|
||||
updatedDashboardLayout[j].y += maxY;
|
||||
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
|
||||
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
|
||||
updatedDashboardLayout[j].i
|
||||
].widgets.map((w) => ({
|
||||
...w,
|
||||
y: w.y + maxY,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
updatedDashboardLayout = updatedDashboardLayout.filter(
|
||||
(widget) => !rowProperties.widgets.some((w: Layout) => w.i === widget.i),
|
||||
);
|
||||
}
|
||||
setCurrentPanelMap((prev) => ({
|
||||
...prev,
|
||||
...updatedPanelMap,
|
||||
[id]: {
|
||||
...rowProperties,
|
||||
},
|
||||
}));
|
||||
|
||||
setDashboardLayout(sortLayout(updatedDashboardLayout));
|
||||
const { updatedLayout, updatedPanelMap } = applyRowCollapse(
|
||||
id,
|
||||
dashboardLayout,
|
||||
currentPanelMap,
|
||||
);
|
||||
setCurrentPanelMap((prev) => ({ ...prev, ...updatedPanelMap }));
|
||||
setDashboardLayout(sortLayout(updatedLayout));
|
||||
};
|
||||
|
||||
const handleDragStop: ItemCallback = (_, oldItem, newItem): void => {
|
||||
|
||||
@@ -4,9 +4,12 @@ import { Button, Popover } from 'antd';
|
||||
import useComponentPermission from 'hooks/useComponentPermission';
|
||||
import { EllipsisIcon, PenLine, Plus, X } from 'lucide-react';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { usePanelTypeSelectionModalStore } from 'providers/Dashboard/helpers/panelTypeSelectionModalHelper';
|
||||
import { setSelectedRowWidgetId } from 'providers/Dashboard/helpers/selectedRowWidgetIdHelper';
|
||||
import {
|
||||
selectIsDashboardLocked,
|
||||
useDashboardStore,
|
||||
} from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { ROLES, USER_ROLES } from 'types/roles';
|
||||
import { ComponentTypes } from 'utils/permission';
|
||||
|
||||
@@ -39,7 +42,8 @@ export function WidgetRowHeader(props: WidgetRowHeaderProps): JSX.Element {
|
||||
(s) => s.setIsPanelTypeSelectionModalOpen,
|
||||
);
|
||||
|
||||
const { selectedDashboard, isDashboardLocked } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
const isDashboardLocked = useDashboardStore(selectIsDashboardLocked);
|
||||
|
||||
const permissions: ComponentTypes[] = ['add_panel'];
|
||||
const { user } = useAppContext();
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { Layout } from 'react-grid-layout';
|
||||
|
||||
import { applyRowCollapse, PanelMap } from '../utils';
|
||||
|
||||
// Helper to produce deeply-frozen objects that mimic what zustand/immer returns.
|
||||
function freeze<T>(obj: T): T {
|
||||
return JSON.parse(JSON.stringify(obj), (_, v) =>
|
||||
typeof v === 'object' && v !== null ? Object.freeze(v) : v,
|
||||
) as T;
|
||||
}
|
||||
|
||||
// ─── fixtures ────────────────────────────────────────────────────────────────
|
||||
|
||||
const ROW_ID = 'row1';
|
||||
|
||||
/** A layout with one row followed by two widgets. */
|
||||
function makeLayout(): Layout[] {
|
||||
return [
|
||||
{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 },
|
||||
{ i: 'w1', x: 0, y: 1, w: 6, h: 4 },
|
||||
{ i: 'w2', x: 6, y: 1, w: 6, h: 4 },
|
||||
];
|
||||
}
|
||||
|
||||
/** panelMap where the row is expanded (collapsed = false, widgets = []). */
|
||||
function makeExpandedPanelMap(): PanelMap {
|
||||
return {
|
||||
[ROW_ID]: { collapsed: false, widgets: [] },
|
||||
};
|
||||
}
|
||||
|
||||
/** panelMap where the row is collapsed (widgets stored inside). */
|
||||
function makeCollapsedPanelMap(): PanelMap {
|
||||
return {
|
||||
[ROW_ID]: {
|
||||
collapsed: true,
|
||||
widgets: [
|
||||
{ i: 'w1', x: 0, y: 1, w: 6, h: 4 },
|
||||
{ i: 'w2', x: 6, y: 1, w: 6, h: 4 },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ─── frozen-input guard (regression for zustand/immer read-only bug) ──────────
|
||||
|
||||
describe('applyRowCollapse – does not mutate frozen inputs', () => {
|
||||
it('does not throw when collapsing a row with frozen layout + panelMap', () => {
|
||||
expect(() =>
|
||||
applyRowCollapse(
|
||||
ROW_ID,
|
||||
freeze(makeLayout()),
|
||||
freeze(makeExpandedPanelMap()),
|
||||
),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('does not throw when expanding a row with frozen layout + panelMap', () => {
|
||||
// Collapsed layout only has the row item; widgets live in panelMap.
|
||||
const collapsedLayout = freeze([{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 }]);
|
||||
expect(() =>
|
||||
applyRowCollapse(ROW_ID, collapsedLayout, freeze(makeCollapsedPanelMap())),
|
||||
).not.toThrow();
|
||||
});
|
||||
|
||||
it('leaves the original layout array untouched after collapse', () => {
|
||||
const layout = makeLayout();
|
||||
const originalY = layout[1].y; // w1.y before collapse
|
||||
applyRowCollapse(ROW_ID, layout, makeExpandedPanelMap());
|
||||
expect(layout[1].y).toBe(originalY);
|
||||
});
|
||||
|
||||
it('leaves the original panelMap untouched after collapse', () => {
|
||||
const panelMap = makeExpandedPanelMap();
|
||||
applyRowCollapse(ROW_ID, makeLayout(), panelMap);
|
||||
expect(panelMap[ROW_ID].collapsed).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── collapse behaviour ───────────────────────────────────────────────────────
|
||||
|
||||
describe('applyRowCollapse – collapsing a row', () => {
|
||||
it('sets collapsed = true on the row entry', () => {
|
||||
const { updatedPanelMap } = applyRowCollapse(
|
||||
ROW_ID,
|
||||
makeLayout(),
|
||||
makeExpandedPanelMap(),
|
||||
);
|
||||
expect(updatedPanelMap[ROW_ID].collapsed).toBe(true);
|
||||
});
|
||||
|
||||
it('stores the child widgets inside the panelMap entry', () => {
|
||||
const { updatedPanelMap } = applyRowCollapse(
|
||||
ROW_ID,
|
||||
makeLayout(),
|
||||
makeExpandedPanelMap(),
|
||||
);
|
||||
const ids = updatedPanelMap[ROW_ID].widgets.map((w) => w.i);
|
||||
expect(ids).toContain('w1');
|
||||
expect(ids).toContain('w2');
|
||||
});
|
||||
|
||||
it('removes child widgets from the returned layout', () => {
|
||||
const { updatedLayout } = applyRowCollapse(
|
||||
ROW_ID,
|
||||
makeLayout(),
|
||||
makeExpandedPanelMap(),
|
||||
);
|
||||
const ids = updatedLayout.map((l) => l.i);
|
||||
expect(ids).not.toContain('w1');
|
||||
expect(ids).not.toContain('w2');
|
||||
expect(ids).toContain(ROW_ID);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── expand behaviour ─────────────────────────────────────────────────────────
|
||||
|
||||
describe('applyRowCollapse – expanding a row', () => {
|
||||
it('sets collapsed = false on the row entry', () => {
|
||||
const collapsedLayout: Layout[] = [{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 }];
|
||||
const { updatedPanelMap } = applyRowCollapse(
|
||||
ROW_ID,
|
||||
collapsedLayout,
|
||||
makeCollapsedPanelMap(),
|
||||
);
|
||||
expect(updatedPanelMap[ROW_ID].collapsed).toBe(false);
|
||||
});
|
||||
|
||||
it('restores child widgets to the returned layout', () => {
|
||||
const collapsedLayout: Layout[] = [{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 }];
|
||||
const { updatedLayout } = applyRowCollapse(
|
||||
ROW_ID,
|
||||
collapsedLayout,
|
||||
makeCollapsedPanelMap(),
|
||||
);
|
||||
const ids = updatedLayout.map((l) => l.i);
|
||||
expect(ids).toContain('w1');
|
||||
expect(ids).toContain('w2');
|
||||
});
|
||||
|
||||
it('restored child widgets appear in both the layout and the panelMap entry', () => {
|
||||
const collapsedLayout: Layout[] = [{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 }];
|
||||
const { updatedLayout, updatedPanelMap } = applyRowCollapse(
|
||||
ROW_ID,
|
||||
collapsedLayout,
|
||||
makeCollapsedPanelMap(),
|
||||
);
|
||||
// The previously-stored widgets should now be back in the live layout.
|
||||
expect(updatedLayout.map((l) => l.i)).toContain('w1');
|
||||
// The panelMap entry still holds a reference to them (stale until next collapse).
|
||||
expect(updatedPanelMap[ROW_ID].widgets.map((w) => w.i)).toContain('w1');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── y-offset adjustment ──────────────────────────────────────────────────────
|
||||
|
||||
describe('applyRowCollapse – y-offset adjustments for rows below', () => {
|
||||
it('shifts items below a second row down when the first row expands', () => {
|
||||
const ROW2 = 'row2';
|
||||
// Layout: row1 (y=0,h=1) | w1 (y=1,h=4) | row2 (y=5,h=1) | w3 (y=6,h=2)
|
||||
const layout: Layout[] = [
|
||||
{ i: ROW_ID, x: 0, y: 0, w: 12, h: 1 },
|
||||
{ i: 'w1', x: 0, y: 1, w: 12, h: 4 },
|
||||
{ i: ROW2, x: 0, y: 5, w: 12, h: 1 },
|
||||
{ i: 'w3', x: 0, y: 6, w: 12, h: 2 },
|
||||
];
|
||||
const panelMap: PanelMap = {
|
||||
[ROW_ID]: {
|
||||
collapsed: true,
|
||||
widgets: [{ i: 'w1', x: 0, y: 1, w: 12, h: 4 }],
|
||||
},
|
||||
[ROW2]: { collapsed: false, widgets: [] },
|
||||
};
|
||||
// Expanding row1 should push row2 and w3 down by the height of w1 (4).
|
||||
const collapsedLayout = layout.filter((l) => l.i !== 'w1');
|
||||
const { updatedLayout } = applyRowCollapse(ROW_ID, collapsedLayout, panelMap);
|
||||
|
||||
const row2Item = updatedLayout.find((l) => l.i === ROW2);
|
||||
expect(row2Item?.y).toBe(5 + 4); // shifted by maxY = 4
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { useDashboard } from 'providers/Dashboard/Dashboard';
|
||||
import { useDashboardStore } from 'providers/Dashboard/store/useDashboardStore';
|
||||
import { Widgets } from 'types/api/dashboard/getAll';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
@@ -121,7 +121,7 @@ function useNavigateToExplorerPages(): (
|
||||
) => Promise<{
|
||||
[queryName: string]: { filters: TagFilterItem[]; dataSource?: string };
|
||||
}> {
|
||||
const { selectedDashboard } = useDashboard();
|
||||
const { selectedDashboard } = useDashboardStore();
|
||||
const { notifications } = useNotifications();
|
||||
|
||||
return useCallback(
|
||||
|
||||
@@ -4,6 +4,122 @@ import { isEmpty, isEqual } from 'lodash-es';
|
||||
import { Dashboard, Widgets } from 'types/api/dashboard/getAll';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export type PanelMap = Record<
|
||||
string,
|
||||
{ widgets: Layout[]; collapsed: boolean }
|
||||
>;
|
||||
|
||||
export interface RowCollapseResult {
|
||||
updatedLayout: Layout[];
|
||||
updatedPanelMap: PanelMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pure function that computes the new layout and panelMap after toggling a
|
||||
* row's collapsed state. All inputs are treated as immutable — no input object
|
||||
* is mutated, so it is safe to pass frozen objects from the zustand store.
|
||||
*/
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function applyRowCollapse(
|
||||
id: string,
|
||||
dashboardLayout: Layout[],
|
||||
currentPanelMap: PanelMap,
|
||||
): RowCollapseResult {
|
||||
// Deep-copy the row's own properties so we can mutate our local copy.
|
||||
const rowProperties = {
|
||||
...currentPanelMap[id],
|
||||
widgets: [...(currentPanelMap[id]?.widgets ?? [])],
|
||||
};
|
||||
|
||||
// Shallow-copy each entry's widgets array so inner .map() calls are safe.
|
||||
const updatedPanelMap: PanelMap = Object.fromEntries(
|
||||
Object.entries(currentPanelMap).map(([k, v]) => [
|
||||
k,
|
||||
{ ...v, widgets: [...v.widgets] },
|
||||
]),
|
||||
);
|
||||
|
||||
let updatedDashboardLayout = [...dashboardLayout];
|
||||
|
||||
if (rowProperties.collapsed === true) {
|
||||
// ── EXPAND ──────────────────────────────────────────────────────────────
|
||||
rowProperties.collapsed = false;
|
||||
const widgetsInsideTheRow = rowProperties.widgets;
|
||||
|
||||
let maxY = 0;
|
||||
widgetsInsideTheRow.forEach((w) => {
|
||||
maxY = Math.max(maxY, w.y + w.h);
|
||||
});
|
||||
const currentRowWidget = dashboardLayout.find((w) => w.i === id);
|
||||
if (currentRowWidget && widgetsInsideTheRow.length) {
|
||||
maxY -= currentRowWidget.h + currentRowWidget.y;
|
||||
}
|
||||
|
||||
const idxCurrentRow = dashboardLayout.findIndex((w) => w.i === id);
|
||||
for (let j = idxCurrentRow + 1; j < dashboardLayout.length; j++) {
|
||||
updatedDashboardLayout[j] = {
|
||||
...updatedDashboardLayout[j],
|
||||
y: updatedDashboardLayout[j].y + maxY,
|
||||
};
|
||||
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
|
||||
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
|
||||
updatedDashboardLayout[j].i
|
||||
].widgets.map((w) => ({ ...w, y: w.y + maxY }));
|
||||
}
|
||||
}
|
||||
updatedDashboardLayout = [...updatedDashboardLayout, ...widgetsInsideTheRow];
|
||||
} else {
|
||||
// ── COLLAPSE ─────────────────────────────────────────────────────────────
|
||||
rowProperties.collapsed = true;
|
||||
const currentIdx = dashboardLayout.findIndex((w) => w.i === id);
|
||||
|
||||
let widgetsInsideTheRow: Layout[] = [];
|
||||
let isPanelMapUpdated = false;
|
||||
for (let j = currentIdx + 1; j < dashboardLayout.length; j++) {
|
||||
if (currentPanelMap[dashboardLayout[j].i]) {
|
||||
rowProperties.widgets = widgetsInsideTheRow;
|
||||
widgetsInsideTheRow = [];
|
||||
isPanelMapUpdated = true;
|
||||
break;
|
||||
} else {
|
||||
widgetsInsideTheRow.push(dashboardLayout[j]);
|
||||
}
|
||||
}
|
||||
if (!isPanelMapUpdated) {
|
||||
rowProperties.widgets = widgetsInsideTheRow;
|
||||
}
|
||||
|
||||
let maxY = 0;
|
||||
widgetsInsideTheRow.forEach((w) => {
|
||||
maxY = Math.max(maxY, w.y + w.h);
|
||||
});
|
||||
const currentRowWidget = dashboardLayout[currentIdx];
|
||||
if (currentRowWidget && widgetsInsideTheRow.length) {
|
||||
maxY -= currentRowWidget.h + currentRowWidget.y;
|
||||
}
|
||||
|
||||
for (let j = currentIdx + 1; j < updatedDashboardLayout.length; j++) {
|
||||
updatedDashboardLayout[j] = {
|
||||
...updatedDashboardLayout[j],
|
||||
y: updatedDashboardLayout[j].y + maxY,
|
||||
};
|
||||
if (updatedPanelMap[updatedDashboardLayout[j].i]) {
|
||||
updatedPanelMap[updatedDashboardLayout[j].i].widgets = updatedPanelMap[
|
||||
updatedDashboardLayout[j].i
|
||||
].widgets.map((w) => ({ ...w, y: w.y + maxY }));
|
||||
}
|
||||
}
|
||||
|
||||
updatedDashboardLayout = updatedDashboardLayout.filter(
|
||||
(widget) => !rowProperties.widgets.some((w: Layout) => w.i === widget.i),
|
||||
);
|
||||
}
|
||||
|
||||
updatedPanelMap[id] = { ...rowProperties };
|
||||
|
||||
return { updatedLayout: updatedDashboardLayout, updatedPanelMap };
|
||||
}
|
||||
|
||||
export const removeUndefinedValuesFromLayout = (layout: Layout[]): Layout[] =>
|
||||
layout.map((obj) =>
|
||||
Object.fromEntries(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
/* eslint-disable sonarjs/no-duplicate-string */
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Compass, Dot, House, Plus, Wrench } from '@signozhq/icons';
|
||||
@@ -16,9 +17,11 @@ import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { getMetricsListQuery } from 'container/MetricsExplorer/Summary/utils';
|
||||
import { IS_SERVICE_ACCOUNTS_ENABLED } from 'container/ServiceAccountsSettings/config';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { useGetMetricsList } from 'hooks/metricsExplorer/useGetMetricsList';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import history from 'lib/history';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
@@ -29,6 +32,7 @@ import { UserPreference } from 'types/api/preferences/preference';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isIngestionActive } from 'utils/app';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import AlertRules from './AlertRules/AlertRules';
|
||||
@@ -47,6 +51,7 @@ const homeInterval = 30 * 60 * 1000;
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function Home(): JSX.Element {
|
||||
const { user } = useAppContext();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
const [startTime, setStartTime] = useState<number | null>(null);
|
||||
@@ -77,7 +82,7 @@ export default function Home(): JSX.Element {
|
||||
query: initialQueriesMap[DataSource.LOGS],
|
||||
graphType: PANEL_TYPES.VALUE,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: '30m',
|
||||
globalSelectedInterval: DEFAULT_TIME_RANGE,
|
||||
params: {
|
||||
dataSource: DataSource.LOGS,
|
||||
},
|
||||
@@ -87,7 +92,7 @@ export default function Home(): JSX.Element {
|
||||
{
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
'30m',
|
||||
DEFAULT_TIME_RANGE,
|
||||
endTime || Date.now(),
|
||||
startTime || Date.now(),
|
||||
initialQueriesMap[DataSource.LOGS],
|
||||
@@ -102,7 +107,7 @@ export default function Home(): JSX.Element {
|
||||
query: initialQueriesMap[DataSource.TRACES],
|
||||
graphType: PANEL_TYPES.VALUE,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
globalSelectedInterval: '30m',
|
||||
globalSelectedInterval: DEFAULT_TIME_RANGE,
|
||||
params: {
|
||||
dataSource: DataSource.TRACES,
|
||||
},
|
||||
@@ -112,7 +117,7 @@ export default function Home(): JSX.Element {
|
||||
{
|
||||
queryKey: [
|
||||
REACT_QUERY_KEY.GET_QUERY_RANGE,
|
||||
'30m',
|
||||
DEFAULT_TIME_RANGE,
|
||||
endTime || Date.now(),
|
||||
startTime || Date.now(),
|
||||
initialQueriesMap[DataSource.TRACES],
|
||||
@@ -393,11 +398,14 @@ export default function Home(): JSX.Element {
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className="active-ingestion-card-actions"
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
// eslint-disable-next-line sonarjs/no-duplicate-string
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
safeNavigate(ROUTES.LOGS_EXPLORER, {
|
||||
newTab: isModifierKeyPressed(e),
|
||||
});
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -434,11 +442,13 @@ export default function Home(): JSX.Element {
|
||||
className="active-ingestion-card-actions"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
safeNavigate(ROUTES.TRACES_EXPLORER, {
|
||||
newTab: isModifierKeyPressed(e),
|
||||
});
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -475,11 +485,13 @@ export default function Home(): JSX.Element {
|
||||
className="active-ingestion-card-actions"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Ingestion Active Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
history.push(ROUTES.METRICS_EXPLORER);
|
||||
safeNavigate(ROUTES.METRICS_EXPLORER, {
|
||||
newTab: isModifierKeyPressed(e),
|
||||
});
|
||||
}}
|
||||
onKeyDown={(e): void => {
|
||||
if (e.key === 'Enter') {
|
||||
@@ -529,11 +541,13 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Logs',
|
||||
});
|
||||
history.push(ROUTES.LOGS_EXPLORER);
|
||||
safeNavigate(ROUTES.LOGS_EXPLORER, {
|
||||
newTab: isModifierKeyPressed(e),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Open Logs Explorer
|
||||
@@ -543,11 +557,13 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Traces',
|
||||
});
|
||||
history.push(ROUTES.TRACES_EXPLORER);
|
||||
safeNavigate(ROUTES.TRACES_EXPLORER, {
|
||||
newTab: isModifierKeyPressed(e),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Open Traces Explorer
|
||||
@@ -557,11 +573,13 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Wrench size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Metrics',
|
||||
});
|
||||
history.push(ROUTES.METRICS_EXPLORER_EXPLORER);
|
||||
safeNavigate(ROUTES.METRICS_EXPLORER_EXPLORER, {
|
||||
newTab: isModifierKeyPressed(e),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Open Metrics Explorer
|
||||
@@ -598,11 +616,13 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Dashboards',
|
||||
});
|
||||
history.push(ROUTES.ALL_DASHBOARD);
|
||||
safeNavigate(ROUTES.ALL_DASHBOARD, {
|
||||
newTab: isModifierKeyPressed(e),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Create dashboard
|
||||
@@ -640,11 +660,13 @@ export default function Home(): JSX.Element {
|
||||
type="default"
|
||||
className="periscope-btn secondary"
|
||||
icon={<Plus size={14} />}
|
||||
onClick={(): void => {
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Explore clicked', {
|
||||
source: 'Alerts',
|
||||
});
|
||||
history.push(ROUTES.ALERTS_NEW);
|
||||
safeNavigate(ROUTES.ALERTS_NEW, {
|
||||
newTab: isModifierKeyPressed(e),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Create an alert
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { QueryKey } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
@@ -30,6 +30,7 @@ import { ServicesList } from 'types/api/metrics/getService';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { Tags } from 'types/reducer/trace';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import { FeatureKeys } from '../../../constants/features';
|
||||
import { DOCS_LINKS } from '../constants';
|
||||
@@ -117,7 +118,7 @@ const ServicesListTable = memo(
|
||||
onRowClick,
|
||||
}: {
|
||||
services: ServicesList[];
|
||||
onRowClick: (record: ServicesList) => void;
|
||||
onRowClick: (record: ServicesList, event: React.MouseEvent) => void;
|
||||
}): JSX.Element => (
|
||||
<div className="services-list-container home-data-item-container metrics-services-list">
|
||||
<div className="services-list">
|
||||
@@ -126,8 +127,8 @@ const ServicesListTable = memo(
|
||||
dataSource={services}
|
||||
pagination={false}
|
||||
className="services-table"
|
||||
onRow={(record): { onClick: () => void } => ({
|
||||
onClick: (): void => onRowClick(record),
|
||||
onRow={(record: ServicesList): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => onRowClick(record, event),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
@@ -285,11 +286,13 @@ function ServiceMetrics({
|
||||
}, [onUpdateChecklistDoneItem, loadingUserPreferences, servicesExist]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
(record: ServicesList) => {
|
||||
(record: ServicesList, event: React.MouseEvent) => {
|
||||
logEvent('Homepage: Service clicked', {
|
||||
serviceName: record.serviceName,
|
||||
});
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, {
|
||||
newTab: isModifierKeyPressed(event),
|
||||
});
|
||||
},
|
||||
[safeNavigate],
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSelector } from 'react-redux'; // old code, TODO: fix this correctly
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button, Select, Skeleton, Table } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -16,6 +16,7 @@ import { LicensePlatform } from 'types/api/licensesV3/getActive';
|
||||
import { ServicesList } from 'types/api/metrics/getService';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
|
||||
import { DOCS_LINKS } from '../constants';
|
||||
import { columns, TIME_PICKER_OPTIONS } from './constants';
|
||||
@@ -173,13 +174,15 @@ export default function ServiceTraces({
|
||||
dataSource={top5Services}
|
||||
pagination={false}
|
||||
className="services-table"
|
||||
onRow={(record): { onClick: () => void } => ({
|
||||
onClick: (): void => {
|
||||
onRow={(record: ServicesList): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => {
|
||||
logEvent('Homepage: Service clicked', {
|
||||
serviceName: record.serviceName,
|
||||
});
|
||||
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`);
|
||||
safeNavigate(`${ROUTES.APPLICATION}/${record.serviceName}`, {
|
||||
newTab: isModifierKeyPressed(event),
|
||||
});
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSearchParams } from 'react-router-dom-v5-compat';
|
||||
import { VerticalAlignTopOutlined } from '@ant-design/icons';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
@@ -11,15 +10,16 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import {
|
||||
getFiltersFromParams,
|
||||
getOrderByFromParams,
|
||||
} from 'container/InfraMonitoringK8s/commonUtils';
|
||||
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from 'container/InfraMonitoringK8s/constants';
|
||||
useInfraMonitoringCurrentPage,
|
||||
useInfraMonitoringFiltersHosts,
|
||||
useInfraMonitoringOrderByHosts,
|
||||
} from 'container/InfraMonitoringK8s/hooks';
|
||||
import { usePageSize } from 'container/InfraMonitoringK8s/utils';
|
||||
import { useGetHostList } from 'hooks/infraMonitoring/useGetHostList';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Filter } from 'lucide-react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
@@ -35,50 +35,29 @@ function HostsList(): JSX.Element {
|
||||
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
|
||||
(state) => state.globalTime,
|
||||
);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [filters, setFilters] = useState<IBuilderQuery['filters']>(() => {
|
||||
const filters = getFiltersFromParams(
|
||||
searchParams,
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS,
|
||||
);
|
||||
if (!filters) {
|
||||
return {
|
||||
items: [],
|
||||
op: 'and',
|
||||
};
|
||||
}
|
||||
return filters;
|
||||
});
|
||||
const [currentPage, setCurrentPage] = useInfraMonitoringCurrentPage();
|
||||
const [filters, setFilters] = useInfraMonitoringFiltersHosts();
|
||||
const [orderBy, setOrderBy] = useInfraMonitoringOrderByHosts();
|
||||
|
||||
const [showFilters, setShowFilters] = useState<boolean>(true);
|
||||
|
||||
const [orderBy, setOrderBy] = useState<{
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
} | null>(() => getOrderByFromParams(searchParams));
|
||||
const [selectedHostName, setSelectedHostName] = useQueryState(
|
||||
'hostName',
|
||||
parseAsString.withDefault(''),
|
||||
);
|
||||
|
||||
const handleOrderByChange = (
|
||||
orderBy: {
|
||||
orderByValue: {
|
||||
columnName: string;
|
||||
order: 'asc' | 'desc';
|
||||
} | null,
|
||||
): void => {
|
||||
setOrderBy(orderBy);
|
||||
setSearchParams({
|
||||
...Object.fromEntries(searchParams.entries()),
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.ORDER_BY]: JSON.stringify(orderBy),
|
||||
});
|
||||
setOrderBy(orderByValue);
|
||||
};
|
||||
|
||||
const [selectedHostName, setSelectedHostName] = useState<string | null>(() => {
|
||||
const hostName = searchParams.get('hostName');
|
||||
return hostName || null;
|
||||
});
|
||||
|
||||
const handleHostClick = (hostName: string): void => {
|
||||
setSelectedHostName(hostName);
|
||||
setSearchParams({ ...searchParams, hostName });
|
||||
};
|
||||
|
||||
const { pageSize, setPageSize } = usePageSize('hosts');
|
||||
@@ -89,7 +68,7 @@ function HostsList(): JSX.Element {
|
||||
...baseQuery,
|
||||
limit: pageSize,
|
||||
offset: (currentPage - 1) * pageSize,
|
||||
filters,
|
||||
filters: filters?.items?.length ? filters : undefined,
|
||||
start: Math.floor(minTime / 1000000),
|
||||
end: Math.floor(maxTime / 1000000),
|
||||
orderBy,
|
||||
@@ -97,15 +76,6 @@ function HostsList(): JSX.Element {
|
||||
}, [pageSize, currentPage, filters, minTime, maxTime, orderBy]);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
if (selectedHostName) {
|
||||
return [
|
||||
'hostList',
|
||||
String(pageSize),
|
||||
String(currentPage),
|
||||
JSON.stringify(filters),
|
||||
JSON.stringify(orderBy),
|
||||
];
|
||||
}
|
||||
return [
|
||||
'hostList',
|
||||
String(pageSize),
|
||||
@@ -115,15 +85,7 @@ function HostsList(): JSX.Element {
|
||||
String(minTime),
|
||||
String(maxTime),
|
||||
];
|
||||
}, [
|
||||
pageSize,
|
||||
currentPage,
|
||||
filters,
|
||||
orderBy,
|
||||
selectedHostName,
|
||||
minTime,
|
||||
maxTime,
|
||||
]);
|
||||
}, [pageSize, currentPage, filters, orderBy, minTime, maxTime]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useGetHostList(
|
||||
query as HostListPayload,
|
||||
@@ -154,12 +116,8 @@ function HostsList(): JSX.Element {
|
||||
const handleFiltersChange = useCallback(
|
||||
(value: IBuilderQuery['filters']): void => {
|
||||
const isNewFilterAdded = value?.items?.length !== filters?.items?.length;
|
||||
setFilters(value);
|
||||
setFilters(value ?? null);
|
||||
handleChangeQueryData('filters', value);
|
||||
setSearchParams({
|
||||
...Object.fromEntries(searchParams.entries()),
|
||||
[INFRA_MONITORING_K8S_PARAMS_KEYS.FILTERS]: JSON.stringify(value),
|
||||
});
|
||||
if (isNewFilterAdded) {
|
||||
setCurrentPage(1);
|
||||
|
||||
@@ -171,8 +129,7 @@ function HostsList(): JSX.Element {
|
||||
}
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[filters],
|
||||
[filters, setFilters, setCurrentPage, handleChangeQueryData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -184,7 +141,7 @@ function HostsList(): JSX.Element {
|
||||
}, [data?.payload?.data?.total]);
|
||||
|
||||
const selectedHostData = useMemo(() => {
|
||||
if (!selectedHostName) {
|
||||
if (!selectedHostName?.trim()) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
@@ -260,6 +217,7 @@ function HostsList(): JSX.Element {
|
||||
pageSize={pageSize}
|
||||
setPageSize={setPageSize}
|
||||
setOrderBy={handleOrderByChange}
|
||||
orderBy={orderBy}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ function HostsListControls({
|
||||
showAutoRefresh,
|
||||
}: {
|
||||
handleFiltersChange: (value: IBuilderQuery['filters']) => void;
|
||||
filters: IBuilderQuery['filters'];
|
||||
filters: IBuilderQuery['filters'] | null;
|
||||
showAutoRefresh: boolean;
|
||||
}): JSX.Element {
|
||||
const currentQuery = initialQueriesMap[DataSource.METRICS];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Skeleton,
|
||||
@@ -11,6 +11,8 @@ import {
|
||||
import type { SorterResult } from 'antd/es/table/interface';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import HostsEmptyOrIncorrectMetrics from './HostsEmptyOrIncorrectMetrics';
|
||||
import {
|
||||
@@ -114,8 +116,12 @@ export default function HostsListTable({
|
||||
pageSize,
|
||||
setOrderBy,
|
||||
setPageSize,
|
||||
orderBy,
|
||||
}: HostsListTableProps): JSX.Element {
|
||||
const columns = useMemo(() => getHostsListColumns(), []);
|
||||
const [defaultOrderBy] = useState(orderBy);
|
||||
const columns = useMemo(() => getHostsListColumns(defaultOrderBy), [
|
||||
defaultOrderBy,
|
||||
]);
|
||||
|
||||
const sentAnyHostMetricsData = useMemo(
|
||||
() => data?.payload?.data?.sentAnyHostMetricsData || false,
|
||||
@@ -162,7 +168,16 @@ export default function HostsListTable({
|
||||
[],
|
||||
);
|
||||
|
||||
const handleRowClick = (record: HostRowData): void => {
|
||||
const handleRowClick = (
|
||||
record: HostRowData,
|
||||
event: React.MouseEvent,
|
||||
): void => {
|
||||
if (isModifierKeyPressed(event)) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set('hostName', record.hostName);
|
||||
openInNewTab(`${window.location.pathname}?${params.toString()}`);
|
||||
return;
|
||||
}
|
||||
onHostClick(record.hostName);
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.HostEntity,
|
||||
@@ -235,8 +250,8 @@ export default function HostsListTable({
|
||||
(record as HostRowData & { key: string }).key ?? record.hostName
|
||||
}
|
||||
onChange={handleTableChange}
|
||||
onRow={(record): { onClick: () => void; className: string } => ({
|
||||
onClick: (): void => handleRowClick(record),
|
||||
onRow={(record: HostRowData): Record<string, unknown> => ({
|
||||
onClick: (event: React.MouseEvent): void => handleRowClick(record, event),
|
||||
className: 'clickable-row',
|
||||
})}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render } from '@testing-library/react';
|
||||
import * as useGetHostListHooks from 'hooks/infraMonitoring/useGetHostList';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import * as timezoneHooks from 'providers/Timezone';
|
||||
import store from 'store';
|
||||
@@ -19,6 +20,16 @@ jest.mock('lib/getMinMax', () => ({
|
||||
isValidShortHandDateTimeFormat: jest.fn().mockReturnValue(true),
|
||||
})),
|
||||
}));
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="date-time-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
jest.mock('components/HostMetricsDetail', () => ({
|
||||
__esModule: true,
|
||||
default: (): null => null,
|
||||
}));
|
||||
jest.mock('components/CustomTimePicker/CustomTimePicker', () => ({
|
||||
__esModule: true,
|
||||
default: ({ onSelect, selectedTime, selectedValue }: any): JSX.Element => (
|
||||
@@ -55,19 +66,6 @@ jest.mock('react-router-dom', () => {
|
||||
}),
|
||||
};
|
||||
});
|
||||
jest.mock('react-router-dom-v5-compat', () => {
|
||||
const actual = jest.requireActual('react-router-dom-v5-compat');
|
||||
return {
|
||||
...actual,
|
||||
useSearchParams: jest
|
||||
.fn()
|
||||
.mockReturnValue([
|
||||
{ get: jest.fn(), entries: jest.fn().mockReturnValue([]) },
|
||||
jest.fn(),
|
||||
]),
|
||||
useNavigationType: (): any => 'PUSH',
|
||||
};
|
||||
});
|
||||
jest.mock('hooks/useSafeNavigate', () => ({
|
||||
useSafeNavigate: (): any => ({
|
||||
safeNavigate: jest.fn(),
|
||||
@@ -130,26 +128,30 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
|
||||
describe('HostsList', () => {
|
||||
it('renders hosts list table', () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
<NuqsTestingAdapter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</NuqsTestingAdapter>,
|
||||
);
|
||||
expect(container.querySelector('.hosts-list-table')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders filters', () => {
|
||||
const { container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
<NuqsTestingAdapter>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<HostsList />
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
</NuqsTestingAdapter>,
|
||||
);
|
||||
expect(container.querySelector('.filters')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -72,6 +72,7 @@ describe('HostsListTable', () => {
|
||||
pageSize: 10,
|
||||
setOrderBy: mockSetOrderBy,
|
||||
setPageSize: mockSetPageSize,
|
||||
orderBy: null,
|
||||
};
|
||||
|
||||
it('renders loading state if isLoading is true and tableData is empty', () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Progress, TabsProps, Tag, Tooltip, Typography } from 'antd';
|
||||
import { TableColumnType as ColumnType } from 'antd';
|
||||
import { SortOrder } from 'antd/lib/table/interface';
|
||||
import {
|
||||
HostData,
|
||||
HostListPayload,
|
||||
@@ -20,6 +21,7 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { OrderBySchemaType } from '../InfraMonitoringK8s/schemas';
|
||||
import HostsList from './HostsList';
|
||||
|
||||
import './InfraMonitoring.styles.scss';
|
||||
@@ -105,6 +107,7 @@ export interface HostsListTableProps {
|
||||
orderBy: { columnName: string; order: 'asc' | 'desc' } | null,
|
||||
) => void;
|
||||
setPageSize: (pageSize: number) => void;
|
||||
orderBy: OrderBySchemaType;
|
||||
}
|
||||
|
||||
export interface EmptyOrLoadingViewProps {
|
||||
@@ -127,6 +130,17 @@ export const getHostListsQuery = (): HostListPayload => ({
|
||||
orderBy: { columnName: 'cpu', order: 'desc' },
|
||||
});
|
||||
|
||||
function mapOrderByToSortOrder(
|
||||
column: string,
|
||||
orderBy: OrderBySchemaType,
|
||||
): SortOrder | undefined {
|
||||
return orderBy?.columnName === column
|
||||
? orderBy?.order === 'asc'
|
||||
? 'ascend'
|
||||
: 'descend'
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export const getTabsItems = (): TabsProps['items'] => [
|
||||
{
|
||||
label: <TabLabel label="List View" isDisabled={false} tooltipText="" />,
|
||||
@@ -135,7 +149,9 @@ export const getTabsItems = (): TabsProps['items'] => [
|
||||
},
|
||||
];
|
||||
|
||||
export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
|
||||
export const getHostsListColumns = (
|
||||
orderBy: OrderBySchemaType,
|
||||
): ColumnType<HostRowData>[] => [
|
||||
{
|
||||
title: <div className="hostname-column-header">Hostname</div>,
|
||||
dataIndex: 'hostName',
|
||||
@@ -164,6 +180,7 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
|
||||
key: 'cpu',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
defaultSortOrder: mapOrderByToSortOrder('cpu', orderBy),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
@@ -179,6 +196,7 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
|
||||
key: 'memory',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
defaultSortOrder: mapOrderByToSortOrder('memory', orderBy),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
@@ -187,6 +205,7 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
|
||||
key: 'wait',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
defaultSortOrder: mapOrderByToSortOrder('wait', orderBy),
|
||||
align: 'right',
|
||||
},
|
||||
{
|
||||
@@ -195,6 +214,7 @@ export const getHostsListColumns = (): ColumnType<HostRowData>[] => [
|
||||
key: 'load15',
|
||||
width: 100,
|
||||
sorter: true,
|
||||
defaultSortOrder: mapOrderByToSortOrder('load15', orderBy),
|
||||
align: 'right',
|
||||
},
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user