mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-28 06:30:33 +01:00
Compare commits
8 Commits
cancel-sub
...
feat/globa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d0f38053e | ||
|
|
f629c8dc08 | ||
|
|
0854c60bc8 | ||
|
|
127924c076 | ||
|
|
5f5f9c7a64 | ||
|
|
564e672a4b | ||
|
|
6f93eecfd6 | ||
|
|
a3d2b503aa |
@@ -4496,184 +4496,6 @@ components:
|
||||
type: object
|
||||
Sigv4SigV4Config:
|
||||
type: object
|
||||
SpantypesFieldContext:
|
||||
enum:
|
||||
- attribute
|
||||
- resource
|
||||
type: string
|
||||
SpantypesGettableSpanMapperGroups:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroup'
|
||||
type: array
|
||||
required:
|
||||
- items
|
||||
type: object
|
||||
SpantypesPostableSpanMapper:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperConfig'
|
||||
enabled:
|
||||
type: boolean
|
||||
field_context:
|
||||
$ref: '#/components/schemas/SpantypesFieldContext'
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- field_context
|
||||
- config
|
||||
type: object
|
||||
SpantypesPostableSpanMapperGroup:
|
||||
properties:
|
||||
category:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroupCategory'
|
||||
condition:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroupCondition'
|
||||
enabled:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- category
|
||||
- condition
|
||||
type: object
|
||||
SpantypesSpanMapper:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperConfig'
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
field_context:
|
||||
$ref: '#/components/schemas/SpantypesFieldContext'
|
||||
group_id:
|
||||
type: string
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- group_id
|
||||
- name
|
||||
- field_context
|
||||
- config
|
||||
- enabled
|
||||
type: object
|
||||
SpantypesSpanMapperConfig:
|
||||
properties:
|
||||
sources:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperSource'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- sources
|
||||
type: object
|
||||
SpantypesSpanMapperGroup:
|
||||
properties:
|
||||
category:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroupCategory'
|
||||
condition:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroupCondition'
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
id:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
orgId:
|
||||
type: string
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- orgId
|
||||
- name
|
||||
- category
|
||||
- condition
|
||||
- enabled
|
||||
type: object
|
||||
SpantypesSpanMapperGroupCategory:
|
||||
type: object
|
||||
SpantypesSpanMapperGroupCondition:
|
||||
properties:
|
||||
attributes:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
resource:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- attributes
|
||||
- resource
|
||||
type: object
|
||||
SpantypesSpanMapperOperation:
|
||||
enum:
|
||||
- move
|
||||
- copy
|
||||
type: string
|
||||
SpantypesSpanMapperSource:
|
||||
properties:
|
||||
context:
|
||||
$ref: '#/components/schemas/SpantypesFieldContext'
|
||||
key:
|
||||
type: string
|
||||
operation:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperOperation'
|
||||
priority:
|
||||
type: integer
|
||||
required:
|
||||
- key
|
||||
- context
|
||||
- operation
|
||||
- priority
|
||||
type: object
|
||||
SpantypesUpdatableSpanMapper:
|
||||
properties:
|
||||
config:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperConfig'
|
||||
enabled:
|
||||
nullable: true
|
||||
type: boolean
|
||||
field_context:
|
||||
$ref: '#/components/schemas/SpantypesFieldContext'
|
||||
type: object
|
||||
SpantypesUpdatableSpanMapperGroup:
|
||||
properties:
|
||||
condition:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroupCondition'
|
||||
enabled:
|
||||
nullable: true
|
||||
type: boolean
|
||||
name:
|
||||
nullable: true
|
||||
type: string
|
||||
type: object
|
||||
TelemetrytypesFieldContext:
|
||||
enum:
|
||||
- metric
|
||||
@@ -9416,487 +9238,6 @@ paths:
|
||||
summary: Updates my service account
|
||||
tags:
|
||||
- serviceaccount
|
||||
/api/v1/span_mapper_groups:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns all span attribute mapping groups for the authenticated
|
||||
org.
|
||||
operationId: ListSpanMapperGroups
|
||||
parameters:
|
||||
- explode: true
|
||||
in: query
|
||||
name: category
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroupCategory'
|
||||
style: deepObject
|
||||
- in: query
|
||||
name: enabled
|
||||
schema:
|
||||
nullable: true
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesGettableSpanMapperGroups'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List span attribute mapping groups
|
||||
tags:
|
||||
- spanmapper
|
||||
post:
|
||||
deprecated: false
|
||||
description: Creates a new span attribute mapping group for the org.
|
||||
operationId: CreateSpanMapperGroup
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpantypesPostableSpanMapperGroup'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroup'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create a span attribute mapping group
|
||||
tags:
|
||||
- spanmapper
|
||||
/api/v1/span_mapper_groups/{groupId}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: Hard-deletes a mapping group and cascades to all its mappers.
|
||||
operationId: DeleteSpanMapperGroup
|
||||
parameters:
|
||||
- in: path
|
||||
name: groupId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Delete a span attribute mapping group
|
||||
tags:
|
||||
- spanmapper
|
||||
patch:
|
||||
deprecated: false
|
||||
description: Partially updates an existing mapping group's name, condition,
|
||||
or enabled state.
|
||||
operationId: UpdateSpanMapperGroup
|
||||
parameters:
|
||||
- in: path
|
||||
name: groupId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpantypesUpdatableSpanMapperGroup'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Update a span attribute mapping group
|
||||
tags:
|
||||
- spanmapper
|
||||
/api/v1/span_mapper_groups/{groupId}/span_mappers:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns all mappers belonging to a mapping group.
|
||||
operationId: ListSpanMappers
|
||||
parameters:
|
||||
- in: path
|
||||
name: groupId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesGettableSpanMapperGroups'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: List span mappers for a group
|
||||
tags:
|
||||
- spanmapper
|
||||
post:
|
||||
deprecated: false
|
||||
description: Adds a new mapper to the specified mapping group.
|
||||
operationId: CreateSpanMapper
|
||||
parameters:
|
||||
- in: path
|
||||
name: groupId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpantypesPostableSpanMapper'
|
||||
responses:
|
||||
"201":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapper'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: Created
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"409":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Conflict
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create a span mapper
|
||||
tags:
|
||||
- spanmapper
|
||||
/api/v1/span_mapper_groups/{groupId}/span_mappers/{mapperId}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: Hard-deletes a mapper from a mapping group.
|
||||
operationId: DeleteSpanMapper
|
||||
parameters:
|
||||
- in: path
|
||||
name: groupId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: mapperId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Delete a span mapper
|
||||
tags:
|
||||
- spanmapper
|
||||
patch:
|
||||
deprecated: false
|
||||
description: Partially updates an existing mapper's field context, config, or
|
||||
enabled state.
|
||||
operationId: UpdateSpanMapper
|
||||
parameters:
|
||||
- in: path
|
||||
name: groupId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- in: path
|
||||
name: mapperId
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpantypesUpdatableSpanMapper'
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Update a span mapper
|
||||
tags:
|
||||
- spanmapper
|
||||
/api/v1/testChannel:
|
||||
post:
|
||||
deprecated: true
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/knip@5/schema.json",
|
||||
"project": ["src/**/*.ts", "src/**/*.tsx"],
|
||||
"ignore": ["src/api/generated/**/*.ts"]
|
||||
}
|
||||
@@ -231,6 +231,7 @@
|
||||
"ts-jest": "29.4.6",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript-plugin-css-modules": "5.2.0",
|
||||
"use-sync-external-store": "1.6.0",
|
||||
"vite-plugin-checker": "0.12.0",
|
||||
"vite-plugin-compression": "0.5.1",
|
||||
"vite-plugin-image-optimizer": "2.0.3",
|
||||
@@ -263,4 +264,4 @@
|
||||
"tmp": "0.2.4",
|
||||
"vite": "npm:rolldown-vite@7.3.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ function createMockLicense(
|
||||
status: '',
|
||||
updated_at: '0',
|
||||
},
|
||||
state: LicenseState.ACTIVATED,
|
||||
state: LicenseState.ACTIVE,
|
||||
status: LicenseStatus.VALID,
|
||||
platform: LicensePlatform.CLOUD,
|
||||
created_at: '0',
|
||||
@@ -931,7 +931,7 @@ describe('PrivateRoute', () => {
|
||||
isFetchingActiveLicense: false,
|
||||
activeLicense: createMockLicense({
|
||||
platform: LicensePlatform.CLOUD,
|
||||
state: LicenseState.ACTIVATED,
|
||||
state: LicenseState.ACTIVE,
|
||||
}),
|
||||
},
|
||||
isCloudUser: true,
|
||||
@@ -1522,7 +1522,7 @@ describe('PrivateRoute', () => {
|
||||
isFetchingActiveLicense: false,
|
||||
activeLicense: createMockLicense({
|
||||
platform: LicensePlatform.CLOUD,
|
||||
state: LicenseState.ACTIVATED,
|
||||
state: LicenseState.ACTIVE,
|
||||
}),
|
||||
trialInfo: createMockTrialInfo({ workSpaceBlock: false }),
|
||||
user: createMockUser({ role: USER_ROLES.ADMIN as ROLES }),
|
||||
|
||||
@@ -5472,187 +5472,6 @@ export interface Sigv4SigV4ConfigDTO {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export enum SpantypesFieldContextDTO {
|
||||
attribute = 'attribute',
|
||||
resource = 'resource',
|
||||
}
|
||||
export interface SpantypesGettableSpanMapperGroupsDTO {
|
||||
/**
|
||||
* @type array
|
||||
*/
|
||||
items: SpantypesSpanMapperGroupDTO[];
|
||||
}
|
||||
|
||||
export interface SpantypesPostableSpanMapperDTO {
|
||||
config: SpantypesSpanMapperConfigDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
field_context: SpantypesFieldContextDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SpantypesPostableSpanMapperGroupDTO {
|
||||
category: SpantypesSpanMapperGroupCategoryDTO;
|
||||
condition: SpantypesSpanMapperGroupConditionDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperDTO {
|
||||
config: SpantypesSpanMapperConfigDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
field_context: SpantypesFieldContextDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
group_id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperConfigDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
sources: SpantypesSpanMapperSourceDTO[] | null;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperGroupDTO {
|
||||
category: SpantypesSpanMapperGroupCategoryDTO;
|
||||
condition: SpantypesSpanMapperGroupConditionDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperGroupCategoryDTO {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanMapperGroupConditionDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
attributes: string[] | null;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
resource: string[] | null;
|
||||
}
|
||||
|
||||
export enum SpantypesSpanMapperOperationDTO {
|
||||
move = 'move',
|
||||
copy = 'copy',
|
||||
}
|
||||
export interface SpantypesSpanMapperSourceDTO {
|
||||
context: SpantypesFieldContextDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
key: string;
|
||||
operation: SpantypesSpanMapperOperationDTO;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface SpantypesUpdatableSpanMapperDTO {
|
||||
config?: SpantypesSpanMapperConfigDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
* @nullable true
|
||||
*/
|
||||
enabled?: boolean | null;
|
||||
field_context?: SpantypesFieldContextDTO;
|
||||
}
|
||||
|
||||
export interface SpantypesUpdatableSpanMapperGroupDTO {
|
||||
condition?: SpantypesSpanMapperGroupConditionDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
* @nullable true
|
||||
*/
|
||||
enabled?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
* @nullable true
|
||||
*/
|
||||
name?: string | null;
|
||||
}
|
||||
|
||||
export enum TelemetrytypesFieldContextDTO {
|
||||
metric = 'metric',
|
||||
log = 'log',
|
||||
@@ -7103,71 +6922,6 @@ export type GetMyServiceAccount200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListSpanMapperGroupsParams = {
|
||||
/**
|
||||
* @description undefined
|
||||
*/
|
||||
category?: SpantypesSpanMapperGroupCategoryDTO;
|
||||
/**
|
||||
* @type boolean
|
||||
* @nullable true
|
||||
* @description undefined
|
||||
*/
|
||||
enabled?: boolean | null;
|
||||
};
|
||||
|
||||
export type ListSpanMapperGroups200 = {
|
||||
data: SpantypesGettableSpanMapperGroupsDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateSpanMapperGroup201 = {
|
||||
data: SpantypesSpanMapperGroupDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteSpanMapperGroupPathParameters = {
|
||||
groupId: string;
|
||||
};
|
||||
export type UpdateSpanMapperGroupPathParameters = {
|
||||
groupId: string;
|
||||
};
|
||||
export type ListSpanMappersPathParameters = {
|
||||
groupId: string;
|
||||
};
|
||||
export type ListSpanMappers200 = {
|
||||
data: SpantypesGettableSpanMapperGroupsDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type CreateSpanMapperPathParameters = {
|
||||
groupId: string;
|
||||
};
|
||||
export type CreateSpanMapper201 = {
|
||||
data: SpantypesSpanMapperDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteSpanMapperPathParameters = {
|
||||
groupId: string;
|
||||
mapperId: string;
|
||||
};
|
||||
export type UpdateSpanMapperPathParameters = {
|
||||
groupId: string;
|
||||
mapperId: string;
|
||||
};
|
||||
export type ListUsersDeprecated200 = {
|
||||
/**
|
||||
* @type array
|
||||
|
||||
@@ -1,787 +0,0 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'yarn generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
CreateSpanMapper201,
|
||||
CreateSpanMapperGroup201,
|
||||
CreateSpanMapperPathParameters,
|
||||
DeleteSpanMapperGroupPathParameters,
|
||||
DeleteSpanMapperPathParameters,
|
||||
ListSpanMapperGroups200,
|
||||
ListSpanMapperGroupsParams,
|
||||
ListSpanMappers200,
|
||||
ListSpanMappersPathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
SpantypesPostableSpanMapperDTO,
|
||||
SpantypesPostableSpanMapperGroupDTO,
|
||||
SpantypesUpdatableSpanMapperDTO,
|
||||
SpantypesUpdatableSpanMapperGroupDTO,
|
||||
UpdateSpanMapperGroupPathParameters,
|
||||
UpdateSpanMapperPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* Returns all span attribute mapping groups for the authenticated org.
|
||||
* @summary List span attribute mapping groups
|
||||
*/
|
||||
export const listSpanMapperGroups = (
|
||||
params?: ListSpanMapperGroupsParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListSpanMapperGroups200>({
|
||||
url: `/api/v1/span_mapper_groups`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListSpanMapperGroupsQueryKey = (
|
||||
params?: ListSpanMapperGroupsParams,
|
||||
) => {
|
||||
return [`/api/v1/span_mapper_groups`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getListSpanMapperGroupsQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listSpanMapperGroups>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListSpanMapperGroupsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listSpanMapperGroups>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getListSpanMapperGroupsQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof listSpanMapperGroups>>
|
||||
> = ({ signal }) => listSpanMapperGroups(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listSpanMapperGroups>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListSpanMapperGroupsQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listSpanMapperGroups>>
|
||||
>;
|
||||
export type ListSpanMapperGroupsQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List span attribute mapping groups
|
||||
*/
|
||||
|
||||
export function useListSpanMapperGroups<
|
||||
TData = Awaited<ReturnType<typeof listSpanMapperGroups>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListSpanMapperGroupsParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listSpanMapperGroups>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListSpanMapperGroupsQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List span attribute mapping groups
|
||||
*/
|
||||
export const invalidateListSpanMapperGroups = async (
|
||||
queryClient: QueryClient,
|
||||
params?: ListSpanMapperGroupsParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListSpanMapperGroupsQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new span attribute mapping group for the org.
|
||||
* @summary Create a span attribute mapping group
|
||||
*/
|
||||
export const createSpanMapperGroup = (
|
||||
spantypesPostableSpanMapperGroupDTO: BodyType<SpantypesPostableSpanMapperGroupDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateSpanMapperGroup201>({
|
||||
url: `/api/v1/span_mapper_groups`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: spantypesPostableSpanMapperGroupDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateSpanMapperGroupMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSpanMapperGroup>>,
|
||||
TError,
|
||||
{ data: BodyType<SpantypesPostableSpanMapperGroupDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSpanMapperGroup>>,
|
||||
TError,
|
||||
{ data: BodyType<SpantypesPostableSpanMapperGroupDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createSpanMapperGroup'];
|
||||
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 createSpanMapperGroup>>,
|
||||
{ data: BodyType<SpantypesPostableSpanMapperGroupDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createSpanMapperGroup(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateSpanMapperGroupMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createSpanMapperGroup>>
|
||||
>;
|
||||
export type CreateSpanMapperGroupMutationBody =
|
||||
BodyType<SpantypesPostableSpanMapperGroupDTO>;
|
||||
export type CreateSpanMapperGroupMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create a span attribute mapping group
|
||||
*/
|
||||
export const useCreateSpanMapperGroup = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSpanMapperGroup>>,
|
||||
TError,
|
||||
{ data: BodyType<SpantypesPostableSpanMapperGroupDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createSpanMapperGroup>>,
|
||||
TError,
|
||||
{ data: BodyType<SpantypesPostableSpanMapperGroupDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getCreateSpanMapperGroupMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Hard-deletes a mapping group and cascades to all its mappers.
|
||||
* @summary Delete a span attribute mapping group
|
||||
*/
|
||||
export const deleteSpanMapperGroup = ({
|
||||
groupId,
|
||||
}: DeleteSpanMapperGroupPathParameters) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/span_mapper_groups/${groupId}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteSpanMapperGroupMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteSpanMapperGroup>>,
|
||||
TError,
|
||||
{ pathParams: DeleteSpanMapperGroupPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteSpanMapperGroup>>,
|
||||
TError,
|
||||
{ pathParams: DeleteSpanMapperGroupPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteSpanMapperGroup'];
|
||||
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 deleteSpanMapperGroup>>,
|
||||
{ pathParams: DeleteSpanMapperGroupPathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteSpanMapperGroup(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteSpanMapperGroupMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteSpanMapperGroup>>
|
||||
>;
|
||||
|
||||
export type DeleteSpanMapperGroupMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete a span attribute mapping group
|
||||
*/
|
||||
export const useDeleteSpanMapperGroup = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteSpanMapperGroup>>,
|
||||
TError,
|
||||
{ pathParams: DeleteSpanMapperGroupPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteSpanMapperGroup>>,
|
||||
TError,
|
||||
{ pathParams: DeleteSpanMapperGroupPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getDeleteSpanMapperGroupMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Partially updates an existing mapping group's name, condition, or enabled state.
|
||||
* @summary Update a span attribute mapping group
|
||||
*/
|
||||
export const updateSpanMapperGroup = (
|
||||
{ groupId }: UpdateSpanMapperGroupPathParameters,
|
||||
spantypesUpdatableSpanMapperGroupDTO: BodyType<SpantypesUpdatableSpanMapperGroupDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/span_mapper_groups/${groupId}`,
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: spantypesUpdatableSpanMapperGroupDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateSpanMapperGroupMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSpanMapperGroup>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSpanMapperGroupPathParameters;
|
||||
data: BodyType<SpantypesUpdatableSpanMapperGroupDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSpanMapperGroup>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSpanMapperGroupPathParameters;
|
||||
data: BodyType<SpantypesUpdatableSpanMapperGroupDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateSpanMapperGroup'];
|
||||
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 updateSpanMapperGroup>>,
|
||||
{
|
||||
pathParams: UpdateSpanMapperGroupPathParameters;
|
||||
data: BodyType<SpantypesUpdatableSpanMapperGroupDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateSpanMapperGroup(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateSpanMapperGroupMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateSpanMapperGroup>>
|
||||
>;
|
||||
export type UpdateSpanMapperGroupMutationBody =
|
||||
BodyType<SpantypesUpdatableSpanMapperGroupDTO>;
|
||||
export type UpdateSpanMapperGroupMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update a span attribute mapping group
|
||||
*/
|
||||
export const useUpdateSpanMapperGroup = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSpanMapperGroup>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSpanMapperGroupPathParameters;
|
||||
data: BodyType<SpantypesUpdatableSpanMapperGroupDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateSpanMapperGroup>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSpanMapperGroupPathParameters;
|
||||
data: BodyType<SpantypesUpdatableSpanMapperGroupDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUpdateSpanMapperGroupMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Returns all mappers belonging to a mapping group.
|
||||
* @summary List span mappers for a group
|
||||
*/
|
||||
export const listSpanMappers = (
|
||||
{ groupId }: ListSpanMappersPathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListSpanMappers200>({
|
||||
url: `/api/v1/span_mapper_groups/${groupId}/span_mappers`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListSpanMappersQueryKey = ({
|
||||
groupId,
|
||||
}: ListSpanMappersPathParameters) => {
|
||||
return [`/api/v1/span_mapper_groups/${groupId}/span_mappers`] as const;
|
||||
};
|
||||
|
||||
export const getListSpanMappersQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listSpanMappers>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ groupId }: ListSpanMappersPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listSpanMappers>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getListSpanMappersQueryKey({ groupId });
|
||||
|
||||
const queryFn: QueryFunction<Awaited<ReturnType<typeof listSpanMappers>>> = ({
|
||||
signal,
|
||||
}) => listSpanMappers({ groupId }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!groupId,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listSpanMappers>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListSpanMappersQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listSpanMappers>>
|
||||
>;
|
||||
export type ListSpanMappersQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List span mappers for a group
|
||||
*/
|
||||
|
||||
export function useListSpanMappers<
|
||||
TData = Awaited<ReturnType<typeof listSpanMappers>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ groupId }: ListSpanMappersPathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listSpanMappers>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListSpanMappersQueryOptions({ groupId }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List span mappers for a group
|
||||
*/
|
||||
export const invalidateListSpanMappers = async (
|
||||
queryClient: QueryClient,
|
||||
{ groupId }: ListSpanMappersPathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListSpanMappersQueryKey({ groupId }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a new mapper to the specified mapping group.
|
||||
* @summary Create a span mapper
|
||||
*/
|
||||
export const createSpanMapper = (
|
||||
{ groupId }: CreateSpanMapperPathParameters,
|
||||
spantypesPostableSpanMapperDTO: BodyType<SpantypesPostableSpanMapperDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<CreateSpanMapper201>({
|
||||
url: `/api/v1/span_mapper_groups/${groupId}/span_mappers`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: spantypesPostableSpanMapperDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateSpanMapperMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSpanMapper>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateSpanMapperPathParameters;
|
||||
data: BodyType<SpantypesPostableSpanMapperDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSpanMapper>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateSpanMapperPathParameters;
|
||||
data: BodyType<SpantypesPostableSpanMapperDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createSpanMapper'];
|
||||
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 createSpanMapper>>,
|
||||
{
|
||||
pathParams: CreateSpanMapperPathParameters;
|
||||
data: BodyType<SpantypesPostableSpanMapperDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return createSpanMapper(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateSpanMapperMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createSpanMapper>>
|
||||
>;
|
||||
export type CreateSpanMapperMutationBody =
|
||||
BodyType<SpantypesPostableSpanMapperDTO>;
|
||||
export type CreateSpanMapperMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create a span mapper
|
||||
*/
|
||||
export const useCreateSpanMapper = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createSpanMapper>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateSpanMapperPathParameters;
|
||||
data: BodyType<SpantypesPostableSpanMapperDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createSpanMapper>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: CreateSpanMapperPathParameters;
|
||||
data: BodyType<SpantypesPostableSpanMapperDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getCreateSpanMapperMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Hard-deletes a mapper from a mapping group.
|
||||
* @summary Delete a span mapper
|
||||
*/
|
||||
export const deleteSpanMapper = ({
|
||||
groupId,
|
||||
mapperId,
|
||||
}: DeleteSpanMapperPathParameters) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/span_mapper_groups/${groupId}/span_mappers/${mapperId}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteSpanMapperMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteSpanMapper>>,
|
||||
TError,
|
||||
{ pathParams: DeleteSpanMapperPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteSpanMapper>>,
|
||||
TError,
|
||||
{ pathParams: DeleteSpanMapperPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteSpanMapper'];
|
||||
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 deleteSpanMapper>>,
|
||||
{ pathParams: DeleteSpanMapperPathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteSpanMapper(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteSpanMapperMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteSpanMapper>>
|
||||
>;
|
||||
|
||||
export type DeleteSpanMapperMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete a span mapper
|
||||
*/
|
||||
export const useDeleteSpanMapper = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteSpanMapper>>,
|
||||
TError,
|
||||
{ pathParams: DeleteSpanMapperPathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteSpanMapper>>,
|
||||
TError,
|
||||
{ pathParams: DeleteSpanMapperPathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getDeleteSpanMapperMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Partially updates an existing mapper's field context, config, or enabled state.
|
||||
* @summary Update a span mapper
|
||||
*/
|
||||
export const updateSpanMapper = (
|
||||
{ groupId, mapperId }: UpdateSpanMapperPathParameters,
|
||||
spantypesUpdatableSpanMapperDTO: BodyType<SpantypesUpdatableSpanMapperDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/span_mapper_groups/${groupId}/span_mappers/${mapperId}`,
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: spantypesUpdatableSpanMapperDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateSpanMapperMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSpanMapper>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSpanMapperPathParameters;
|
||||
data: BodyType<SpantypesUpdatableSpanMapperDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSpanMapper>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSpanMapperPathParameters;
|
||||
data: BodyType<SpantypesUpdatableSpanMapperDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['updateSpanMapper'];
|
||||
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 updateSpanMapper>>,
|
||||
{
|
||||
pathParams: UpdateSpanMapperPathParameters;
|
||||
data: BodyType<SpantypesUpdatableSpanMapperDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return updateSpanMapper(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateSpanMapperMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateSpanMapper>>
|
||||
>;
|
||||
export type UpdateSpanMapperMutationBody =
|
||||
BodyType<SpantypesUpdatableSpanMapperDTO>;
|
||||
export type UpdateSpanMapperMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Update a span mapper
|
||||
*/
|
||||
export const useUpdateSpanMapper = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateSpanMapper>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSpanMapperPathParameters;
|
||||
data: BodyType<SpantypesUpdatableSpanMapperDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateSpanMapper>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: UpdateSpanMapperPathParameters;
|
||||
data: BodyType<SpantypesUpdatableSpanMapperDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getUpdateSpanMapperMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
@@ -4,13 +4,7 @@ import {
|
||||
notOfTrailResponse,
|
||||
trialConvertedToSubscriptionResponse,
|
||||
} from 'mocks-server/__mockdata__/licenses';
|
||||
import { act, render, screen, getAppContextMock } from 'tests/test-utils';
|
||||
import APIError from 'types/api/error';
|
||||
import {
|
||||
LicensePlatform,
|
||||
LicenseResModel,
|
||||
LicenseState,
|
||||
} from 'types/api/licensesV3/getActive';
|
||||
import { act, render, screen } from 'tests/test-utils';
|
||||
import { getFormattedDate } from 'utils/timeUtils';
|
||||
|
||||
import BillingContainer from './BillingContainer';
|
||||
@@ -26,7 +20,7 @@ window.ResizeObserver =
|
||||
describe('BillingContainer', () => {
|
||||
jest.setTimeout(30000);
|
||||
|
||||
it('Component should render', async () => {
|
||||
test('Component should render', async () => {
|
||||
render(<BillingContainer />);
|
||||
|
||||
const dataInjection = screen.getByRole('columnheader', {
|
||||
@@ -67,7 +61,7 @@ describe('BillingContainer', () => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('OnTrail', async () => {
|
||||
test('OnTrail', async () => {
|
||||
// Pin "now" so trial end (20 Oct 2023) is tomorrow => "1 days_remaining"
|
||||
|
||||
render(
|
||||
@@ -79,19 +73,17 @@ describe('BillingContainer', () => {
|
||||
// If the component schedules any setTimeout on mount, flush them:
|
||||
jest.runOnlyPendingTimers();
|
||||
|
||||
await expect(screen.findByText('Free Trial')).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByText('billing')).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByText(/\$0/i)).resolves.toBeInTheDocument();
|
||||
expect(await screen.findByText('Free Trial')).toBeInTheDocument();
|
||||
expect(await screen.findByText('billing')).toBeInTheDocument();
|
||||
expect(await screen.findByText(/\$0/i)).toBeInTheDocument();
|
||||
|
||||
await expect(
|
||||
screen.findByText(
|
||||
expect(
|
||||
await screen.findByText(
|
||||
/You are in free trial period. Your free trial will end on 20 Oct 2023/i,
|
||||
),
|
||||
).resolves.toBeInTheDocument();
|
||||
).toBeInTheDocument();
|
||||
|
||||
await expect(
|
||||
screen.findByText(/1 days_remaining/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(await screen.findByText(/1 days_remaining/i)).toBeInTheDocument();
|
||||
|
||||
const upgradeButtons = await screen.findAllByRole('button', {
|
||||
name: /upgrade_plan/i,
|
||||
@@ -99,19 +91,13 @@ describe('BillingContainer', () => {
|
||||
expect(upgradeButtons).toHaveLength(2);
|
||||
expect(upgradeButtons[1]).toBeInTheDocument();
|
||||
|
||||
await expect(
|
||||
screen.findByText(/checkout_plans/i),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByRole('link', { name: /here/i }),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
await expect(
|
||||
screen.findByText('Cancel Subscription', { selector: 'span' }),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(await screen.findByText(/checkout_plans/i)).toBeInTheDocument();
|
||||
expect(
|
||||
await screen.findByRole('link', { name: /here/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('OnTrail but trialConvertedToSubscription', async () => {
|
||||
test('OnTrail but trialConvertedToSubscription', async () => {
|
||||
await act(async () => {
|
||||
render(
|
||||
<BillingContainer />,
|
||||
@@ -148,89 +134,10 @@ describe('BillingContainer', () => {
|
||||
const dayRemainingInBillingPeriod =
|
||||
await screen.findByText(/1 days_remaining/i);
|
||||
expect(dayRemainingInBillingPeriod).toBeInTheDocument();
|
||||
|
||||
await expect(
|
||||
screen.findByText('Cancel Subscription', { selector: 'span' }),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CancelSubscriptionBanner visibility', () => {
|
||||
const baseActiveLicense = getAppContextMock('ADMIN')
|
||||
.activeLicense as LicenseResModel;
|
||||
|
||||
it('should render when license is ACTIVATED and platform is CLOUD', async () => {
|
||||
render(<BillingContainer />);
|
||||
await expect(
|
||||
screen.findByText('Cancel Subscription', { selector: 'span' }),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['EXPIRED', LicenseState.EXPIRED],
|
||||
['TERMINATED', LicenseState.TERMINATED],
|
||||
['CANCELLED', LicenseState.CANCELLED],
|
||||
['EVALUATION_EXPIRED', LicenseState.EVALUATION_EXPIRED],
|
||||
['DEFAULTED', LicenseState.DEFAULTED],
|
||||
['ISSUED', LicenseState.ISSUED],
|
||||
['EVALUATING', LicenseState.EVALUATING],
|
||||
])('should not render when license state is %s', async (_, state) => {
|
||||
render(
|
||||
<BillingContainer />,
|
||||
{},
|
||||
{
|
||||
appContextOverrides: {
|
||||
activeLicense: { ...baseActiveLicense, state },
|
||||
},
|
||||
},
|
||||
);
|
||||
await screen.findByText('billing');
|
||||
expect(
|
||||
screen.queryByText('Cancel Subscription', { selector: 'span' }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const makeAPIError = (statusCode: number): APIError =>
|
||||
new APIError({
|
||||
httpStatusCode: statusCode as any,
|
||||
error: { code: 'error', message: 'error', url: '', errors: [] },
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
'Self-Hosted platform',
|
||||
{
|
||||
activeLicense: {
|
||||
...baseActiveLicense,
|
||||
platform: LicensePlatform.SELF_HOSTED,
|
||||
},
|
||||
activeLicenseFetchError: null,
|
||||
},
|
||||
],
|
||||
[
|
||||
'Community Enterprise user (license API 404)',
|
||||
{
|
||||
activeLicense: null,
|
||||
activeLicenseFetchError: makeAPIError(404),
|
||||
},
|
||||
],
|
||||
[
|
||||
'Community user (license API 501)',
|
||||
{
|
||||
activeLicense: null,
|
||||
activeLicenseFetchError: makeAPIError(501),
|
||||
},
|
||||
],
|
||||
])('should not render for %s', async (_, overrides) => {
|
||||
render(<BillingContainer />, {}, { appContextOverrides: overrides });
|
||||
await screen.findByText('billing');
|
||||
expect(
|
||||
screen.queryByText('Cancel Subscription', { selector: 'span' }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('Not on ontrail', async () => {
|
||||
test('Not on ontrail', async () => {
|
||||
const { findByText } = render(
|
||||
<BillingContainer />,
|
||||
{},
|
||||
|
||||
@@ -34,12 +34,10 @@ import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
import { getFormattedDate, getRemainingDays } from 'utils/timeUtils';
|
||||
|
||||
import CancelSubscriptionBanner from './CancelSubscriptionBanner';
|
||||
import { BillingUsageGraph } from './BillingUsageGraph/BillingUsageGraph';
|
||||
import { prepareCsvData } from './BillingUsageGraph/utils';
|
||||
|
||||
import './BillingContainer.styles.scss';
|
||||
import { LicenseState } from 'types/api/licensesV3/getActive';
|
||||
|
||||
interface DataType {
|
||||
key: string;
|
||||
@@ -319,7 +317,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
|
||||
const handleBilling = useCallback(async () => {
|
||||
if (!trialInfo?.trialConvertedToSubscription) {
|
||||
void logEvent('Billing : Upgrade Plan', {
|
||||
logEvent('Billing : Upgrade Plan', {
|
||||
user: pick(user, ['email', 'userId', 'name']),
|
||||
org,
|
||||
});
|
||||
@@ -328,7 +326,7 @@ export default function BillingContainer(): JSX.Element {
|
||||
url: getBaseUrl(),
|
||||
});
|
||||
} else {
|
||||
void logEvent('Billing : Manage Billing', {
|
||||
logEvent('Billing : Manage Billing', {
|
||||
user: pick(user, ['email', 'userId', 'name']),
|
||||
org,
|
||||
});
|
||||
@@ -537,10 +535,6 @@ export default function BillingContainer(): JSX.Element {
|
||||
{(isLoading || isFetchingBillingData) && renderTableSkeleton()}
|
||||
</div>
|
||||
|
||||
{isCloudUserVal && activeLicense?.state === LicenseState.ACTIVATED && (
|
||||
<CancelSubscriptionBanner />
|
||||
)}
|
||||
|
||||
{!trialInfo?.trialConvertedToSubscription && (
|
||||
<div className="upgrade-plan-benefits">
|
||||
<Row
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
.banner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--padding-4);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--callout-error-border);
|
||||
background-color: var(--callout-error-background);
|
||||
margin: var(--spacing-4) 0 var(--spacing-12);
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--callout-error-title);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
line-height: var(--paragraph-base-400-line-height);
|
||||
color: var(--callout-error-icon);
|
||||
}
|
||||
|
||||
.dialogBody {
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-relaxed);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
|
||||
import CancelSubscriptionBanner from './CancelSubscriptionBanner';
|
||||
|
||||
jest.mock('utils/basePath', () => ({
|
||||
getBasePath: (): string => '/',
|
||||
withBasePath: (path: string): string => path,
|
||||
getAbsoluteUrl: (path: string): string => `https://test.signoz.io${path}`,
|
||||
getBaseUrl: (): string => 'https://test.signoz.io',
|
||||
}));
|
||||
|
||||
describe('CancelSubscriptionBanner', () => {
|
||||
it('renders banner with title and subtitle', () => {
|
||||
render(<CancelSubscriptionBanner />);
|
||||
expect(
|
||||
screen.getByText('Cancel Subscription', { selector: 'span' }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText('Cancel your SigNoz subscription.'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens dialog with correct content when Cancel Subscription is clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<CancelSubscriptionBanner />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /cancel subscription/i }),
|
||||
);
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(/reach out to our support team/i),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /keep subscription/i }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: /contact support/i }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('sends mailto to cloud-support with correct subject on Contact Support', async () => {
|
||||
const realCreateElement = document.createElement.bind(document);
|
||||
const mockClick = jest.fn();
|
||||
const mockAnchor = { href: '', click: mockClick };
|
||||
jest.spyOn(document, 'createElement').mockImplementation((tag: string) => {
|
||||
if (tag === 'a') {
|
||||
return mockAnchor as unknown as HTMLAnchorElement;
|
||||
}
|
||||
return realCreateElement(tag);
|
||||
});
|
||||
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<CancelSubscriptionBanner />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /cancel subscription/i }),
|
||||
);
|
||||
await user.click(screen.getByRole('button', { name: /contact support/i }));
|
||||
|
||||
expect(mockAnchor.href).toContain('mailto:cloud-support@signoz.io');
|
||||
expect(mockAnchor.href).toContain('Cancel%20My%20SigNoz%20Subscription');
|
||||
expect(mockClick).toHaveBeenCalledTimes(1);
|
||||
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
});
|
||||
@@ -1,106 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { Button, DialogWrapper } from '@signozhq/ui';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { pick } from 'lodash-es';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
|
||||
import styles from './CancelSubscriptionBanner.module.scss';
|
||||
|
||||
function CancelSubscriptionBanner(): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { user, org } = useAppContext();
|
||||
|
||||
const handleOpenCancelDialog = (): void => {
|
||||
void logEvent('Billing : Cancel Subscription Clicked', {
|
||||
user: pick(user, ['email', 'displayName', 'role', 'organization']),
|
||||
role: user?.role,
|
||||
});
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const handleContactSupport = (): void => {
|
||||
void logEvent('Billing : Cancel Subscription Confirmed', {
|
||||
user: pick(user, ['email', 'displayName', 'role', 'organization']),
|
||||
role: user?.role,
|
||||
});
|
||||
const subject = encodeURIComponent('Cancel My SigNoz Subscription');
|
||||
const orgName = org?.[0]?.displayName ?? '';
|
||||
const body = encodeURIComponent(
|
||||
[
|
||||
'Hi SigNoz Team,',
|
||||
'',
|
||||
'I would like to cancel my SigNoz Cloud subscription.',
|
||||
'Please find my account details below.',
|
||||
'',
|
||||
'Account Details:',
|
||||
` • SigNoz URL: ${getBaseUrl()}`,
|
||||
...(orgName ? [` • Organization: ${orgName}`] : []),
|
||||
` • Account Email: ${user?.email ?? ''}`,
|
||||
'',
|
||||
'Reason for Cancellation:',
|
||||
'[Please share the reason for cancellation]',
|
||||
'',
|
||||
'Additional feedback (optional):',
|
||||
'[Any other feedback]',
|
||||
'',
|
||||
'Regards,',
|
||||
'[user name or team name]',
|
||||
].join('\n'),
|
||||
);
|
||||
const link = document.createElement('a');
|
||||
link.href = `mailto:cloud-support@signoz.io?subject=${subject}&body=${body}`;
|
||||
link.click();
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
const footer = (
|
||||
<>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
onClick={(): void => setOpen(false)}
|
||||
>
|
||||
Keep Subscription
|
||||
</Button>
|
||||
<Button variant="solid" color="destructive" onClick={handleContactSupport}>
|
||||
Contact Support
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.banner}>
|
||||
<div className={styles.info}>
|
||||
<span className={styles.title}>Cancel Subscription</span>
|
||||
<span className={styles.subtitle}>Cancel your SigNoz subscription.</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
prefix={<X size={12} />}
|
||||
onClick={handleOpenCancelDialog}
|
||||
>
|
||||
Cancel Subscription
|
||||
</Button>
|
||||
</div>
|
||||
<DialogWrapper
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title="Cancel your subscription"
|
||||
width="narrow"
|
||||
showCloseButton
|
||||
footer={footer}
|
||||
>
|
||||
<p className={styles.dialogBody}>
|
||||
To cancel your SigNoz subscription, please reach out to our support team.
|
||||
We'll be happy to assist you.
|
||||
</p>
|
||||
</DialogWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CancelSubscriptionBanner;
|
||||
@@ -37,10 +37,7 @@ import {
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { isCustomTimeRange, useGlobalTimeStore } from 'store/globalTime';
|
||||
import {
|
||||
getAutoRefreshQueryKey,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime/utils';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
@@ -190,16 +187,19 @@ function K8sBaseDetails<T>({
|
||||
);
|
||||
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const lastComputedMinMax = useGlobalTimeStore((s) => s.lastComputedMinMax);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
const getAutoRefreshQueryKey = useGlobalTimeStore(
|
||||
(s) => s.getAutoRefreshQueryKey,
|
||||
);
|
||||
|
||||
const { startMs, endMs } = useMemo(() => {
|
||||
const { minTime: startNs, maxTime: endNs } = getMinMaxTime(selectedTime);
|
||||
|
||||
return {
|
||||
startMs: Math.floor(startNs / NANO_SECOND_MULTIPLIER),
|
||||
endMs: Math.floor(endNs / NANO_SECOND_MULTIPLIER),
|
||||
};
|
||||
}, [getMinMaxTime, selectedTime]);
|
||||
const { startMs, endMs } = useMemo(
|
||||
() => ({
|
||||
startMs: Math.floor(lastComputedMinMax.minTime / NANO_SECOND_MULTIPLIER),
|
||||
endMs: Math.floor(lastComputedMinMax.maxTime / NANO_SECOND_MULTIPLIER),
|
||||
}),
|
||||
[lastComputedMinMax],
|
||||
);
|
||||
|
||||
const [modalTimeRange, setModalTimeRange] = useState(() => ({
|
||||
startTime: startMs,
|
||||
@@ -246,7 +246,7 @@ function K8sBaseDetails<T>({
|
||||
`${queryKeyPrefix}EntityDetails`,
|
||||
selectedItem,
|
||||
),
|
||||
[queryKeyPrefix, selectedItem, selectedTime],
|
||||
[getAutoRefreshQueryKey, queryKeyPrefix, selectedItem, selectedTime],
|
||||
);
|
||||
|
||||
const {
|
||||
|
||||
@@ -16,10 +16,7 @@ import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import {
|
||||
getAutoRefreshQueryKey,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime/utils';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
@@ -114,6 +111,9 @@ export function K8sBaseList<T>({
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
const getAutoRefreshQueryKey = useGlobalTimeStore(
|
||||
(s) => s.getAutoRefreshQueryKey,
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
return getAutoRefreshQueryKey(
|
||||
@@ -127,6 +127,7 @@ export function K8sBaseList<T>({
|
||||
JSON.stringify(groupBy),
|
||||
);
|
||||
}, [
|
||||
getAutoRefreshQueryKey,
|
||||
selectedTime,
|
||||
entity,
|
||||
pageSize,
|
||||
|
||||
@@ -11,10 +11,7 @@ import {
|
||||
} from 'antd';
|
||||
import { CornerDownRight } from 'lucide-react';
|
||||
import { useGlobalTimeStore } from 'store/globalTime';
|
||||
import {
|
||||
getAutoRefreshQueryKey,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime/utils';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { buildAbsolutePath, isModifierKeyPressed } from 'utils/app';
|
||||
@@ -118,6 +115,9 @@ export function K8sExpandedRow<T>({
|
||||
const refreshInterval = useGlobalTimeStore((s) => s.refreshInterval);
|
||||
const isRefreshEnabled = useGlobalTimeStore((s) => s.isRefreshEnabled);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
const getAutoRefreshQueryKey = useGlobalTimeStore(
|
||||
(s) => s.getAutoRefreshQueryKey,
|
||||
);
|
||||
|
||||
const queryKey = useMemo(() => {
|
||||
return getAutoRefreshQueryKey(selectedTime, [
|
||||
@@ -126,7 +126,7 @@ export function K8sExpandedRow<T>({
|
||||
JSON.stringify(queryFilters),
|
||||
JSON.stringify(orderBy),
|
||||
]);
|
||||
}, [selectedTime, record.key, queryFilters, orderBy]);
|
||||
}, [getAutoRefreshQueryKey, selectedTime, record.key, queryFilters, orderBy]);
|
||||
|
||||
const { data, isFetching, isLoading, isError } = useQuery({
|
||||
queryKey,
|
||||
|
||||
@@ -27,7 +27,6 @@ const interestedInOptions: Record<string, string> = {
|
||||
singleTool:
|
||||
'Single Tool (logs, metrics & traces) to reduce operational overhead',
|
||||
correlateSignals: 'Correlate signals for faster troubleshooting',
|
||||
openSourceTooling: 'Prefer open-source tooling',
|
||||
};
|
||||
|
||||
export function AboutSigNozQuestions({
|
||||
|
||||
@@ -17,7 +17,7 @@ import dayjs, { Dayjs } from 'dayjs';
|
||||
import {
|
||||
useGlobalTimeQueryInvalidate,
|
||||
useIsGlobalTimeQueryRefreshing,
|
||||
} from 'hooks/globalTime';
|
||||
} from 'store/globalTime';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
@@ -101,14 +101,14 @@ function DateTimeSelection({
|
||||
if (modalInitialStartTime !== undefined) {
|
||||
initialModalStartTime = modalInitialStartTime;
|
||||
} else if (searchStartTime) {
|
||||
initialModalStartTime = parseInt(searchStartTime, 10);
|
||||
initialModalStartTime = Number.parseInt(searchStartTime, 10);
|
||||
}
|
||||
|
||||
let initialModalEndTime = 0;
|
||||
if (modalInitialEndTime !== undefined) {
|
||||
initialModalEndTime = modalInitialEndTime;
|
||||
} else if (searchEndTime) {
|
||||
initialModalEndTime = parseInt(searchEndTime, 10);
|
||||
initialModalEndTime = Number.parseInt(searchEndTime, 10);
|
||||
}
|
||||
|
||||
const [modalStartTime, setModalStartTime] = useState<number>(
|
||||
@@ -159,9 +159,11 @@ function DateTimeSelection({
|
||||
const getTime = useCallback((): [number, number] | undefined => {
|
||||
if (searchEndTime && searchStartTime) {
|
||||
const startDate = dayjs(
|
||||
new Date(parseInt(getTimeString(searchStartTime), 10)),
|
||||
new Date(Number.parseInt(getTimeString(searchStartTime), 10)),
|
||||
);
|
||||
const endDate = dayjs(
|
||||
new Date(Number.parseInt(getTimeString(searchEndTime), 10)),
|
||||
);
|
||||
const endDate = dayjs(new Date(parseInt(getTimeString(searchEndTime), 10)));
|
||||
|
||||
return [startDate.toDate().getTime() || 0, endDate.toDate().getTime() || 0];
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export { useGlobalTimeQueryInvalidate } from './useGlobalTimeQueryInvalidate';
|
||||
export { useIsGlobalTimeQueryRefreshing } from './useIsGlobalTimeQueryRefreshing';
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
|
||||
/**
|
||||
* Use when you want to invalida any query tracked by {@link REACT_QUERY_KEY.AUTO_REFRESH_QUERY}
|
||||
*/
|
||||
export function useGlobalTimeQueryInvalidate(): () => Promise<void> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useCallback(async () => {
|
||||
return await queryClient.invalidateQueries({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY],
|
||||
});
|
||||
}, [queryClient]);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useIsFetching } from 'react-query';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
|
||||
/**
|
||||
* Use when you want to know if any query tracked by {@link REACT_QUERY_KEY.AUTO_REFRESH_QUERY} is refreshing
|
||||
*/
|
||||
export function useIsGlobalTimeQueryRefreshing(): boolean {
|
||||
return (
|
||||
useIsFetching({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY],
|
||||
}) > 0
|
||||
);
|
||||
}
|
||||
22
frontend/src/pages/AlertChannelCreate/config.tsx
Normal file
22
frontend/src/pages/AlertChannelCreate/config.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import ROUTES from 'constants/routes';
|
||||
import CreateAlertChannels from 'container/CreateAlertChannels';
|
||||
import { ChannelType } from 'container/CreateAlertChannels/config';
|
||||
import GeneralSettings from 'container/GeneralSettings';
|
||||
import { t } from 'i18next';
|
||||
|
||||
export const alertsRoutesConfig = [
|
||||
{
|
||||
Component: GeneralSettings,
|
||||
name: t('routes.general'),
|
||||
route: ROUTES.SETTINGS,
|
||||
key: ROUTES.SETTINGS,
|
||||
},
|
||||
{
|
||||
Component: (): JSX.Element => (
|
||||
<CreateAlertChannels preType={ChannelType.Slack} />
|
||||
),
|
||||
name: t('routes.alert_channels'),
|
||||
route: ROUTES.CHANNELS_NEW,
|
||||
key: ROUTES.CHANNELS_NEW,
|
||||
},
|
||||
];
|
||||
19
frontend/src/pages/AlertChannelCreate/index.tsx
Normal file
19
frontend/src/pages/AlertChannelCreate/index.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import RouteTab from 'components/RouteTab';
|
||||
import history from 'lib/history';
|
||||
|
||||
import { alertsRoutesConfig } from './config';
|
||||
|
||||
function SettingsPage(): JSX.Element {
|
||||
const { pathname } = useLocation();
|
||||
|
||||
return (
|
||||
<RouteTab
|
||||
history={history}
|
||||
routes={alertsRoutesConfig}
|
||||
activeKey={pathname}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsPage;
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { CircleCheck, Siren } from 'lucide-react';
|
||||
import { getDurationFromNow } from 'utils/timeUtils';
|
||||
|
||||
import { AlertStatusProps, StatusConfig } from './types';
|
||||
|
||||
import './AlertStatus.styles.scss';
|
||||
|
||||
export default function AlertStatus({
|
||||
status,
|
||||
timestamp,
|
||||
}: AlertStatusProps): JSX.Element {
|
||||
const statusConfig: StatusConfig = useMemo(
|
||||
() => ({
|
||||
firing: {
|
||||
icon: <Siren size={14} color={Color.TEXT_VANILLA_400} />,
|
||||
text: 'Firing since',
|
||||
extraInfo: timestamp ? (
|
||||
<>
|
||||
<div>⎯</div>
|
||||
<div className="time">{getDurationFromNow(timestamp)}</div>
|
||||
</>
|
||||
) : null,
|
||||
className: 'alert-status-info--firing',
|
||||
},
|
||||
resolved: {
|
||||
icon: (
|
||||
<CircleCheck
|
||||
size={14}
|
||||
fill={Color.BG_VANILLA_400}
|
||||
color={Color.BG_INK_400}
|
||||
/>
|
||||
),
|
||||
text: 'Resolved',
|
||||
extraInfo: null,
|
||||
className: 'alert-status-info--resolved',
|
||||
},
|
||||
}),
|
||||
[timestamp],
|
||||
);
|
||||
|
||||
const currentStatus = statusConfig[status];
|
||||
|
||||
return (
|
||||
<div className={`alert-status-info ${currentStatus.className}`}>
|
||||
<div className="alert-status-info__icon">{currentStatus.icon}</div>
|
||||
<div className="alert-status-info__details">
|
||||
<div className="text">{currentStatus.text}</div>
|
||||
{currentStatus.extraInfo}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export type AlertStatusProps =
|
||||
| { status: 'firing'; timestamp: number }
|
||||
| { status: 'resolved'; timestamp?: number };
|
||||
|
||||
export type StatusConfig = {
|
||||
firing: {
|
||||
icon: JSX.Element;
|
||||
text: string;
|
||||
extraInfo: JSX.Element | null;
|
||||
className: string;
|
||||
};
|
||||
resolved: {
|
||||
icon: JSX.Element;
|
||||
text: string;
|
||||
extraInfo: JSX.Element | null;
|
||||
className: string;
|
||||
};
|
||||
};
|
||||
3
frontend/src/pages/AlertHistory/index.tsx
Normal file
3
frontend/src/pages/AlertHistory/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import AlertHistory from 'container/AlertHistory';
|
||||
|
||||
export default AlertHistory;
|
||||
86
frontend/src/pages/AllErrors/utils.tsx
Normal file
86
frontend/src/pages/AllErrors/utils.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
FiltersType,
|
||||
IQuickFiltersConfig,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export const ExceptionsQuickFiltersConfig: IQuickFiltersConfig[] = [
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Environment',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'deployment.environment',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
defaultOpen: true,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Service Name',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'service.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'Hostname',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'host.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'K8s Cluster Name',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'k8s.cluster.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'K8s Deployment Name',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'k8s.deployment.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'K8s Namespace Name',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'k8s.namespace.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
{
|
||||
type: FiltersType.CHECKBOX,
|
||||
title: 'K8s Pod Name',
|
||||
dataSource: DataSource.TRACES,
|
||||
attributeKey: {
|
||||
key: 'k8s.pod.name',
|
||||
dataType: DataTypes.String,
|
||||
type: 'resource',
|
||||
},
|
||||
defaultOpen: false,
|
||||
},
|
||||
];
|
||||
13
frontend/src/pages/Billing/BillingPage.tsx
Normal file
13
frontend/src/pages/Billing/BillingPage.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import BillingContainer from 'container/BillingContainer/BillingContainer';
|
||||
|
||||
import './BillingPage.styles.scss';
|
||||
|
||||
function BillingPage(): JSX.Element {
|
||||
return (
|
||||
<div className="billingPageContainer">
|
||||
<BillingContainer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default BillingPage;
|
||||
3
frontend/src/pages/Billing/index.tsx
Normal file
3
frontend/src/pages/Billing/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import BillingPage from './BillingPage';
|
||||
|
||||
export default BillingPage;
|
||||
18
frontend/src/pages/CreateAlert/styles.ts
Normal file
18
frontend/src/pages/CreateAlert/styles.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Typography } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Title = styled(Typography)`
|
||||
&&& {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ButtonContainer = styled.div`
|
||||
&&& {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`;
|
||||
3
frontend/src/pages/HomePage/index.tsx
Normal file
3
frontend/src/pages/HomePage/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import HomePage from './HomePage';
|
||||
|
||||
export default HomePage;
|
||||
14
frontend/src/pages/LogsExplorer/styles.ts
Normal file
14
frontend/src/pages/LogsExplorer/styles.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Col } from 'antd';
|
||||
import { themeColors } from 'constants/theme';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const WrapperStyled = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
color: ${themeColors.lightWhite};
|
||||
`;
|
||||
|
||||
export const ButtonWrapperStyled = styled(Col)`
|
||||
margin-left: auto;
|
||||
`;
|
||||
7
frontend/src/pages/LogsModulePage/utils.ts
Normal file
7
frontend/src/pages/LogsModulePage/utils.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export const removeSourcePageFromPath = (path: string): string => {
|
||||
const lastSlashIndex = path.lastIndexOf('/');
|
||||
if (lastSlashIndex !== -1) {
|
||||
return path.substring(0, lastSlashIndex);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
6
frontend/src/pages/MySettings/index.tsx
Normal file
6
frontend/src/pages/MySettings/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import MySettingsContainer from 'container/MySettings';
|
||||
|
||||
function MySettings(): JSX.Element {
|
||||
return <MySettingsContainer />;
|
||||
}
|
||||
export default MySettings;
|
||||
25
frontend/src/pages/SomethingWentWrong/index.tsx
Normal file
25
frontend/src/pages/SomethingWentWrong/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Button, Typography } from 'antd';
|
||||
import SomethingWentWrongAsset from 'assets/SomethingWentWrong';
|
||||
import { Container } from 'components/NotFound/styles';
|
||||
import ROUTES from 'constants/routes';
|
||||
import history from 'lib/history';
|
||||
|
||||
function SomethingWentWrong(): JSX.Element {
|
||||
return (
|
||||
<Container>
|
||||
<SomethingWentWrongAsset />
|
||||
<Typography.Title level={3}>Oops! Something went wrong</Typography.Title>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={(): void => {
|
||||
history.push(ROUTES.HOME);
|
||||
}}
|
||||
className="periscope-btn primary"
|
||||
>
|
||||
Return to Home
|
||||
</Button>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default SomethingWentWrong;
|
||||
10
frontend/src/pages/TracesExplorer/styles.ts
Normal file
10
frontend/src/pages/TracesExplorer/styles.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const Container = styled.div`
|
||||
margin: 1rem 0;
|
||||
`;
|
||||
|
||||
export const ActionsWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
71
frontend/src/store/globalTime/GlobalTimeContext.tsx
Normal file
71
frontend/src/store/globalTime/GlobalTimeContext.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
// oxlint-disable-next-line no-restricted-imports
|
||||
createContext,
|
||||
ReactNode,
|
||||
// oxlint-disable-next-line no-restricted-imports
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
|
||||
import get from 'api/browser/localstorage/get';
|
||||
|
||||
import {
|
||||
createGlobalTimeStore,
|
||||
defaultGlobalTimeStore,
|
||||
GlobalTimeStoreApi,
|
||||
} from './globalTimeStore';
|
||||
import { GlobalTimeProviderOptions, GlobalTimeSelectedTime } from './types';
|
||||
import { usePersistence } from './usePersistence';
|
||||
import { useQueryCacheSync } from './useQueryCacheSync';
|
||||
import { useUrlSync } from './useUrlSync';
|
||||
import { useComputedMinMaxSync } from 'store/globalTime/useComputedMinMaxSync';
|
||||
|
||||
export const GlobalTimeContext = createContext<GlobalTimeStoreApi | null>(null);
|
||||
|
||||
export function GlobalTimeProvider({
|
||||
children,
|
||||
name,
|
||||
inheritGlobalTime = false,
|
||||
initialTime,
|
||||
enableUrlParams = false,
|
||||
removeQueryParamsOnUnmount = false,
|
||||
localStoragePersistKey,
|
||||
refreshInterval: initialRefreshInterval,
|
||||
}: GlobalTimeProviderOptions & { children: ReactNode }): JSX.Element {
|
||||
const parentStore = useContext(GlobalTimeContext);
|
||||
const globalStore = parentStore ?? defaultGlobalTimeStore;
|
||||
|
||||
const resolveInitialTime = (): GlobalTimeSelectedTime => {
|
||||
if (inheritGlobalTime) {
|
||||
return globalStore.getState().selectedTime;
|
||||
}
|
||||
if (localStoragePersistKey) {
|
||||
const stored = get(localStoragePersistKey);
|
||||
if (stored) {
|
||||
return stored as GlobalTimeSelectedTime;
|
||||
}
|
||||
}
|
||||
return initialTime ?? DEFAULT_TIME_RANGE;
|
||||
};
|
||||
|
||||
// Create isolated store (stable reference)
|
||||
const [store] = useState(() =>
|
||||
createGlobalTimeStore({
|
||||
name,
|
||||
selectedTime: resolveInitialTime(),
|
||||
refreshInterval: initialRefreshInterval ?? 0,
|
||||
}),
|
||||
);
|
||||
|
||||
useComputedMinMaxSync(store);
|
||||
useQueryCacheSync(store);
|
||||
useUrlSync(store, enableUrlParams, removeQueryParamsOnUnmount);
|
||||
usePersistence(store, localStoragePersistKey);
|
||||
|
||||
return (
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
{children}
|
||||
</GlobalTimeContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,693 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { ReactNode } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
||||
import set from 'api/browser/localstorage/set';
|
||||
|
||||
import { GlobalTimeProvider } from '../GlobalTimeContext';
|
||||
import { useGlobalTime } from '../hooks';
|
||||
import { GlobalTimeProviderOptions } from '../types';
|
||||
import { createCustomTimeRange, NANO_SECOND_MULTIPLIER } from '../utils';
|
||||
|
||||
jest.mock('api/browser/localstorage/set');
|
||||
|
||||
const createTestQueryClient = (): QueryClient =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createWrapper = (
|
||||
providerProps: GlobalTimeProviderOptions,
|
||||
nuqsProps?: { searchParams?: string },
|
||||
) => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter searchParams={nuqsProps?.searchParams}>
|
||||
<GlobalTimeProvider {...providerProps}>{children}</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
describe('GlobalTimeProvider', () => {
|
||||
describe('name prop', () => {
|
||||
it('should pass name to store when provided', () => {
|
||||
const wrapper = createWrapper({ name: 'test-drawer' });
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.name), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current).toBe('test-drawer');
|
||||
});
|
||||
|
||||
it('should have undefined name when not provided', () => {
|
||||
const wrapper = createWrapper({});
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.name), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('store isolation', () => {
|
||||
it('should create isolated store for each provider', () => {
|
||||
const wrapper1 = createWrapper({ initialTime: '1h' });
|
||||
const wrapper2 = createWrapper({ initialTime: '15m' });
|
||||
|
||||
const { result: result1 } = renderHook(
|
||||
() => useGlobalTime((s) => s.selectedTime),
|
||||
{ wrapper: wrapper1 },
|
||||
);
|
||||
const { result: result2 } = renderHook(
|
||||
() => useGlobalTime((s) => s.selectedTime),
|
||||
{ wrapper: wrapper2 },
|
||||
);
|
||||
|
||||
expect(result1.current).toBe('1h');
|
||||
expect(result2.current).toBe('15m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('inheritGlobalTime', () => {
|
||||
it('should inherit time from parent store when inheritGlobalTime is true', () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter>
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime>{children}</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: NestedWrapper,
|
||||
});
|
||||
|
||||
// Should inherit '6h' from parent provider
|
||||
expect(result.current).toBe('6h');
|
||||
});
|
||||
|
||||
it('should use initialTime when inheritGlobalTime is false', () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter>
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime={false} initialTime="15m">
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: NestedWrapper,
|
||||
});
|
||||
|
||||
// Should use its own initialTime, not parent's
|
||||
expect(result.current).toBe('15m');
|
||||
});
|
||||
|
||||
it('should prefer URL params over inheritGlobalTime when both are present', async () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter searchParams="?relativeTime=1h">
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime enableUrlParams>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: NestedWrapper,
|
||||
});
|
||||
|
||||
// inheritGlobalTime sets initial value to '6h', but URL sync updates it to '1h'
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBe('1h');
|
||||
});
|
||||
});
|
||||
|
||||
it('should use inherited time when URL params are empty', async () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter searchParams="">
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime enableUrlParams>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: NestedWrapper,
|
||||
});
|
||||
|
||||
// No URL params, should keep inherited value
|
||||
expect(result.current).toBe('6h');
|
||||
});
|
||||
|
||||
it('should prefer custom time URL params over inheritGlobalTime', async () => {
|
||||
const queryClient = createTestQueryClient();
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
|
||||
const NestedWrapper = ({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={`?startTime=${startTime}&endTime=${endTime}`}
|
||||
>
|
||||
<GlobalTimeProvider initialTime="6h">
|
||||
<GlobalTimeProvider inheritGlobalTime enableUrlParams>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), {
|
||||
wrapper: NestedWrapper,
|
||||
});
|
||||
|
||||
// URL custom time params should override inherited time
|
||||
await waitFor(() => {
|
||||
const { minTime, maxTime } = result.current.getMinMaxTime();
|
||||
expect(minTime).toBe(startTime * NANO_SECOND_MULTIPLIER);
|
||||
expect(maxTime).toBe(endTime * NANO_SECOND_MULTIPLIER);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('URL sync', () => {
|
||||
it('should read relativeTime from URL on mount', async () => {
|
||||
const wrapper = createWrapper(
|
||||
{ enableUrlParams: true },
|
||||
{ searchParams: '?relativeTime=1h' },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBe('1h');
|
||||
});
|
||||
});
|
||||
|
||||
it('should read custom time from URL on mount', async () => {
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
const wrapper = createWrapper(
|
||||
{ enableUrlParams: true },
|
||||
{ searchParams: `?startTime=${startTime}&endTime=${endTime}` },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
const { minTime, maxTime } = result.current.getMinMaxTime();
|
||||
expect(minTime).toBe(startTime * NANO_SECOND_MULTIPLIER);
|
||||
expect(maxTime).toBe(endTime * NANO_SECOND_MULTIPLIER);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom URL keys when provided', async () => {
|
||||
const wrapper = createWrapper(
|
||||
{
|
||||
enableUrlParams: {
|
||||
relativeTimeKey: 'modalTime',
|
||||
},
|
||||
},
|
||||
{ searchParams: '?modalTime=3h' },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBe('3h');
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom startTimeKey and endTimeKey when provided', async () => {
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
const wrapper = createWrapper(
|
||||
{
|
||||
enableUrlParams: {
|
||||
startTimeKey: 'customStart',
|
||||
endTimeKey: 'customEnd',
|
||||
},
|
||||
},
|
||||
{ searchParams: `?customStart=${startTime}&customEnd=${endTime}` },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
const { minTime, maxTime } = result.current.getMinMaxTime();
|
||||
expect(minTime).toBe(startTime * NANO_SECOND_MULTIPLIER);
|
||||
expect(maxTime).toBe(endTime * NANO_SECOND_MULTIPLIER);
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT read from URL when enableUrlParams is false', async () => {
|
||||
const wrapper = createWrapper(
|
||||
{ enableUrlParams: false, initialTime: '15m' },
|
||||
{ searchParams: '?relativeTime=1h' },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Should use initialTime, not URL value
|
||||
expect(result.current).toBe('15m');
|
||||
});
|
||||
|
||||
it('should prefer startTime/endTime over relativeTime when both present in URL', async () => {
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
const wrapper = createWrapper(
|
||||
{ enableUrlParams: true },
|
||||
{
|
||||
searchParams: `?relativeTime=15m&startTime=${startTime}&endTime=${endTime}`,
|
||||
},
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
const { minTime, maxTime } = result.current.getMinMaxTime();
|
||||
// Should use startTime/endTime, not relativeTime
|
||||
expect(minTime).toBe(startTime * NANO_SECOND_MULTIPLIER);
|
||||
expect(maxTime).toBe(endTime * NANO_SECOND_MULTIPLIER);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use initialTime when URL has invalid time values', async () => {
|
||||
const wrapper = createWrapper(
|
||||
{ enableUrlParams: true, initialTime: '15m' },
|
||||
{ searchParams: '?startTime=invalid&endTime=also-invalid' },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// parseAsInteger returns null for invalid values, so should fallback to initialTime
|
||||
expect(result.current).toBe('15m');
|
||||
});
|
||||
|
||||
it('should update store when custom time is set from URL with only startTime and endTime', async () => {
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
const wrapper = createWrapper(
|
||||
{ enableUrlParams: true },
|
||||
{ searchParams: `?startTime=${startTime}&endTime=${endTime}` },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
await waitFor(() => {
|
||||
// Verify selectedTime is a custom time range string
|
||||
expect(result.current.selectedTime).toContain('||_||');
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeQueryParamsOnUnmount', () => {
|
||||
const createUnmountTestWrapper = (
|
||||
getQueryString: () => string,
|
||||
setQueryString: (qs: string) => void,
|
||||
) => {
|
||||
return function TestWrapper({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
const queryClient = createTestQueryClient();
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={getQueryString()}
|
||||
onUrlUpdate={(event): void => {
|
||||
setQueryString(event.queryString);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
it('should remove URL params when provider unmounts with removeQueryParamsOnUnmount=true', async () => {
|
||||
let currentQueryString = 'relativeTime=1h';
|
||||
const TestWrapper = createUnmountTestWrapper(
|
||||
() => currentQueryString,
|
||||
(qs) => {
|
||||
currentQueryString = qs;
|
||||
},
|
||||
);
|
||||
|
||||
const { unmount } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestWrapper>
|
||||
<GlobalTimeProvider enableUrlParams removeQueryParamsOnUnmount>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</TestWrapper>
|
||||
),
|
||||
});
|
||||
|
||||
// Verify initial URL params are present
|
||||
expect(currentQueryString).toContain('relativeTime=1h');
|
||||
|
||||
// Unmount the provider
|
||||
unmount();
|
||||
|
||||
// URL params should be removed
|
||||
await waitFor(() => {
|
||||
expect(currentQueryString).not.toContain('relativeTime');
|
||||
expect(currentQueryString).not.toContain('startTime');
|
||||
expect(currentQueryString).not.toContain('endTime');
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT remove URL params when provider unmounts with removeQueryParamsOnUnmount=false', async () => {
|
||||
let currentQueryString = 'relativeTime=1h';
|
||||
const TestWrapper = createUnmountTestWrapper(
|
||||
() => currentQueryString,
|
||||
(qs) => {
|
||||
currentQueryString = qs;
|
||||
},
|
||||
);
|
||||
|
||||
const { unmount } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestWrapper>
|
||||
<GlobalTimeProvider enableUrlParams removeQueryParamsOnUnmount={false}>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</TestWrapper>
|
||||
),
|
||||
});
|
||||
|
||||
// Verify initial URL params are present
|
||||
expect(currentQueryString).toContain('relativeTime=1h');
|
||||
|
||||
// Unmount the provider
|
||||
unmount();
|
||||
|
||||
// Wait a tick to ensure cleanup effects would have run
|
||||
await waitFor(() => {
|
||||
// URL params should still be present
|
||||
expect(currentQueryString).toContain('relativeTime=1h');
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove custom time URL params on unmount', async () => {
|
||||
const startTime = 1700000000000;
|
||||
const endTime = 1700003600000;
|
||||
let currentQueryString = `startTime=${startTime}&endTime=${endTime}`;
|
||||
const TestWrapper = createUnmountTestWrapper(
|
||||
() => currentQueryString,
|
||||
(qs) => {
|
||||
currentQueryString = qs;
|
||||
},
|
||||
);
|
||||
|
||||
const { unmount } = renderHook(() => useGlobalTime(), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestWrapper>
|
||||
<GlobalTimeProvider enableUrlParams removeQueryParamsOnUnmount>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</TestWrapper>
|
||||
),
|
||||
});
|
||||
|
||||
// Verify initial URL params are present
|
||||
expect(currentQueryString).toContain('startTime');
|
||||
expect(currentQueryString).toContain('endTime');
|
||||
|
||||
// Unmount the provider
|
||||
unmount();
|
||||
|
||||
// URL params should be removed
|
||||
await waitFor(() => {
|
||||
expect(currentQueryString).not.toContain('startTime');
|
||||
expect(currentQueryString).not.toContain('endTime');
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove custom URL key params on unmount', async () => {
|
||||
let currentQueryString = 'modalTime=3h';
|
||||
const TestWrapper = createUnmountTestWrapper(
|
||||
() => currentQueryString,
|
||||
(qs) => {
|
||||
currentQueryString = qs;
|
||||
},
|
||||
);
|
||||
|
||||
const { unmount } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestWrapper>
|
||||
<GlobalTimeProvider
|
||||
enableUrlParams={{
|
||||
relativeTimeKey: 'modalTime',
|
||||
}}
|
||||
removeQueryParamsOnUnmount
|
||||
>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</TestWrapper>
|
||||
),
|
||||
});
|
||||
|
||||
// Verify initial URL params are present
|
||||
expect(currentQueryString).toContain('modalTime=3h');
|
||||
|
||||
// Unmount the provider
|
||||
unmount();
|
||||
|
||||
// URL params should be removed
|
||||
await waitFor(() => {
|
||||
expect(currentQueryString).not.toContain('modalTime');
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT remove URL params when enableUrlParams is false', async () => {
|
||||
let currentQueryString = 'relativeTime=1h';
|
||||
const TestWrapper = createUnmountTestWrapper(
|
||||
() => currentQueryString,
|
||||
(qs) => {
|
||||
currentQueryString = qs;
|
||||
},
|
||||
);
|
||||
|
||||
const { unmount } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper: ({ children }) => (
|
||||
<TestWrapper>
|
||||
<GlobalTimeProvider enableUrlParams={false} removeQueryParamsOnUnmount>
|
||||
{children}
|
||||
</GlobalTimeProvider>
|
||||
</TestWrapper>
|
||||
),
|
||||
});
|
||||
|
||||
// Verify initial URL params are present
|
||||
expect(currentQueryString).toContain('relativeTime=1h');
|
||||
|
||||
// Unmount the provider
|
||||
unmount();
|
||||
|
||||
// Wait a tick
|
||||
await waitFor(() => {
|
||||
// URL params should still be present (enableUrlParams is false)
|
||||
expect(currentQueryString).toContain('relativeTime=1h');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('localStorage persistence', () => {
|
||||
const mockSet = set as jest.MockedFunction<typeof set>;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
mockSet.mockClear();
|
||||
mockSet.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('should read from localStorage on mount', () => {
|
||||
localStorage.setItem('test-time-key', '6h');
|
||||
|
||||
const wrapper = createWrapper({ localStoragePersistKey: 'test-time-key' });
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current).toBe('6h');
|
||||
});
|
||||
|
||||
it('should write to localStorage on selectedTime change', async () => {
|
||||
const wrapper = createWrapper({
|
||||
localStoragePersistKey: 'test-persist-key',
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
mockSet.mockClear();
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('12h');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSet).toHaveBeenCalledWith('test-persist-key', '12h');
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT write to localStorage when persistKey is undefined', async () => {
|
||||
const wrapper = createWrapper({ initialTime: '15m' });
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
mockSet.mockClear();
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('1h');
|
||||
});
|
||||
|
||||
// Wait a tick to ensure any async operations complete
|
||||
await waitFor(() => {
|
||||
expect(result.current.selectedTime).toBe('1h');
|
||||
});
|
||||
|
||||
expect(mockSet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should only write to localStorage when selectedTime changes, not other state', async () => {
|
||||
const wrapper = createWrapper({
|
||||
localStoragePersistKey: 'test-key',
|
||||
initialTime: '15m',
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
mockSet.mockClear();
|
||||
|
||||
// Change refreshInterval (not selectedTime)
|
||||
act(() => {
|
||||
result.current.setRefreshInterval(5000);
|
||||
});
|
||||
|
||||
// Wait to ensure subscription handler had a chance to run
|
||||
await waitFor(() => {
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
// Should NOT have written to localStorage for refreshInterval change
|
||||
expect(mockSet).not.toHaveBeenCalled();
|
||||
|
||||
// Now change selectedTime
|
||||
act(() => {
|
||||
result.current.setSelectedTime('1h');
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSet).toHaveBeenCalledWith('test-key', '1h');
|
||||
});
|
||||
});
|
||||
|
||||
it('should fallback to initialTime when localStorage contains empty string', () => {
|
||||
localStorage.setItem('test-key', '');
|
||||
|
||||
const wrapper = createWrapper({
|
||||
localStoragePersistKey: 'test-key',
|
||||
initialTime: '15m',
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Empty string is falsy, should use initialTime
|
||||
expect(result.current).toBe('15m');
|
||||
});
|
||||
|
||||
it('should write custom time range to localStorage', async () => {
|
||||
const wrapper = createWrapper({
|
||||
localStoragePersistKey: 'test-custom-key',
|
||||
initialTime: '15m',
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
mockSet.mockClear();
|
||||
|
||||
const customTime = createCustomTimeRange(1000000000, 2000000000);
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSet).toHaveBeenCalledWith('test-custom-key', customTime);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('refreshInterval', () => {
|
||||
it('should initialize with provided refreshInterval', () => {
|
||||
const wrapper = createWrapper({ refreshInterval: 5000 });
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
import { act } from '@testing-library/react';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
|
||||
import {
|
||||
createGlobalTimeStore,
|
||||
defaultGlobalTimeStore,
|
||||
} from '../globalTimeStore';
|
||||
import { createCustomTimeRange } from '../utils';
|
||||
|
||||
describe('createGlobalTimeStore', () => {
|
||||
describe('factory function', () => {
|
||||
it('should create independent store instances', () => {
|
||||
const store1 = createGlobalTimeStore();
|
||||
const store2 = createGlobalTimeStore();
|
||||
|
||||
store1.getState().setSelectedTime('1h');
|
||||
|
||||
expect(store1.getState().selectedTime).toBe('1h');
|
||||
expect(store2.getState().selectedTime).toBe(DEFAULT_TIME_RANGE);
|
||||
});
|
||||
|
||||
it('should accept initial state', () => {
|
||||
const store = createGlobalTimeStore({
|
||||
selectedTime: '15m',
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
expect(store.getState().selectedTime).toBe('15m');
|
||||
expect(store.getState().refreshInterval).toBe(5000);
|
||||
expect(store.getState().isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should compute isRefreshEnabled correctly for custom time', () => {
|
||||
const customTime = createCustomTimeRange(1000000000, 2000000000);
|
||||
const store = createGlobalTimeStore({
|
||||
selectedTime: customTime,
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
|
||||
expect(store.getState().isRefreshEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defaultGlobalTimeStore', () => {
|
||||
it('should be a singleton', () => {
|
||||
expect(defaultGlobalTimeStore).toBeDefined();
|
||||
expect(defaultGlobalTimeStore.getState().selectedTime).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setRefreshInterval', () => {
|
||||
it('should update refresh interval and enable refresh', () => {
|
||||
const store = createGlobalTimeStore();
|
||||
|
||||
act(() => {
|
||||
store.getState().setRefreshInterval(10000);
|
||||
});
|
||||
|
||||
expect(store.getState().refreshInterval).toBe(10000);
|
||||
expect(store.getState().isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should disable refresh when interval is 0', () => {
|
||||
const store = createGlobalTimeStore({ refreshInterval: 5000 });
|
||||
|
||||
act(() => {
|
||||
store.getState().setRefreshInterval(0);
|
||||
});
|
||||
|
||||
expect(store.getState().refreshInterval).toBe(0);
|
||||
expect(store.getState().isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should not enable refresh for custom time range', () => {
|
||||
const customTime = createCustomTimeRange(1000000000, 2000000000);
|
||||
const store = createGlobalTimeStore({ selectedTime: customTime });
|
||||
|
||||
act(() => {
|
||||
store.getState().setRefreshInterval(10000);
|
||||
});
|
||||
|
||||
expect(store.getState().refreshInterval).toBe(10000);
|
||||
expect(store.getState().isRefreshEnabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('store name', () => {
|
||||
it('should store name when provided', () => {
|
||||
const store = createGlobalTimeStore({ name: 'drawer' });
|
||||
expect(store.getState().name).toBe('drawer');
|
||||
});
|
||||
|
||||
it('should have undefined name when not provided', () => {
|
||||
const store = createGlobalTimeStore();
|
||||
expect(store.getState().name).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAutoRefreshQueryKey', () => {
|
||||
it('should generate key without name for unnamed store', () => {
|
||||
const store = createGlobalTimeStore();
|
||||
const key = store
|
||||
.getState()
|
||||
.getAutoRefreshQueryKey('15m', 'MY_QUERY', 'param1');
|
||||
|
||||
expect(key).toStrictEqual([
|
||||
REACT_QUERY_KEY.AUTO_REFRESH_QUERY,
|
||||
'MY_QUERY',
|
||||
'param1',
|
||||
'15m',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should generate key with name for named store', () => {
|
||||
const store = createGlobalTimeStore({ name: 'drawer' });
|
||||
const key = store
|
||||
.getState()
|
||||
.getAutoRefreshQueryKey('15m', 'MY_QUERY', 'param1');
|
||||
|
||||
expect(key).toStrictEqual([
|
||||
REACT_QUERY_KEY.AUTO_REFRESH_QUERY,
|
||||
'drawer',
|
||||
'MY_QUERY',
|
||||
'param1',
|
||||
'15m',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle no query parts for named store', () => {
|
||||
const store = createGlobalTimeStore({ name: 'test' });
|
||||
const key = store.getState().getAutoRefreshQueryKey('1h');
|
||||
|
||||
expect(key).toStrictEqual([
|
||||
REACT_QUERY_KEY.AUTO_REFRESH_QUERY,
|
||||
'test',
|
||||
'1h',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle no query parts for unnamed store', () => {
|
||||
const store = createGlobalTimeStore();
|
||||
const key = store.getState().getAutoRefreshQueryKey('1h');
|
||||
|
||||
expect(key).toStrictEqual([REACT_QUERY_KEY.AUTO_REFRESH_QUERY, '1h']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,202 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
|
||||
import { useGlobalTimeStore } from '../globalTimeStore';
|
||||
import { GlobalTimeSelectedTime } from '../types';
|
||||
import { createCustomTimeRange, NANO_SECOND_MULTIPLIER } from '../utils';
|
||||
|
||||
describe('globalTimeStore', () => {
|
||||
beforeEach(() => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
act(() => {
|
||||
result.current.setSelectedTime(DEFAULT_TIME_RANGE, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it(`should have default selectedTime of ${DEFAULT_TIME_RANGE}`, () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.selectedTime).toBe(DEFAULT_TIME_RANGE);
|
||||
});
|
||||
|
||||
it('should have isRefreshEnabled as false by default', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should have refreshInterval as 0 by default', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.refreshInterval).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSelectedTime', () => {
|
||||
it('should update selectedTime', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
expect(result.current.selectedTime).toBe('15m');
|
||||
});
|
||||
|
||||
it('should update refreshInterval when provided', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
it('should keep existing refreshInterval when not provided', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('1h');
|
||||
});
|
||||
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
it('should enable refresh for relative time with refreshInterval > 0', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should disable refresh for relative time with refreshInterval = 0', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 0);
|
||||
});
|
||||
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should disable refresh for custom time range even with refreshInterval > 0', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
const customTime = createCustomTimeRange(1000000000, 2000000000);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime, 5000);
|
||||
});
|
||||
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
it('should handle various relative time formats', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
const timeFormats: GlobalTimeSelectedTime[] = [
|
||||
'1m',
|
||||
'5m',
|
||||
'15m',
|
||||
'30m',
|
||||
'1h',
|
||||
'3h',
|
||||
'6h',
|
||||
'1d',
|
||||
'1w',
|
||||
];
|
||||
|
||||
timeFormats.forEach((time) => {
|
||||
act(() => {
|
||||
result.current.setSelectedTime(time, 10000);
|
||||
});
|
||||
|
||||
expect(result.current.selectedTime).toBe(time);
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMinMaxTime', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return min/max time for custom time range', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
const minTime = 1000000000;
|
||||
const maxTime = 2000000000;
|
||||
const customTime = createCustomTimeRange(minTime, maxTime);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime);
|
||||
});
|
||||
|
||||
const { minTime: resultMin, maxTime: resultMax } =
|
||||
result.current.getMinMaxTime();
|
||||
expect(resultMin).toBe(minTime);
|
||||
expect(resultMax).toBe(maxTime);
|
||||
});
|
||||
|
||||
it('should compute fresh min/max time for relative time', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
const { minTime, maxTime } = result.current.getMinMaxTime();
|
||||
const now = Date.now() * NANO_SECOND_MULTIPLIER;
|
||||
const fifteenMinutesNs = 15 * 60 * 1000 * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
expect(maxTime).toBe(now);
|
||||
expect(minTime).toBe(now - fifteenMinutesNs);
|
||||
});
|
||||
|
||||
it('should return different values on subsequent calls for relative time', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
const first = result.current.getMinMaxTime();
|
||||
|
||||
// Advance time by 1 second
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
const second = result.current.getMinMaxTime();
|
||||
|
||||
// maxTime should be different (1 second later)
|
||||
expect(second.maxTime).toBe(first.maxTime + 1000 * NANO_SECOND_MULTIPLIER);
|
||||
expect(second.minTime).toBe(first.minTime + 1000 * NANO_SECOND_MULTIPLIER);
|
||||
});
|
||||
});
|
||||
|
||||
describe('store isolation', () => {
|
||||
it('should share state between multiple hook instances', () => {
|
||||
const { result: result1 } = renderHook(() => useGlobalTimeStore());
|
||||
const { result: result2 } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result1.current.setSelectedTime('1h', 10000);
|
||||
});
|
||||
|
||||
expect(result2.current.selectedTime).toBe('1h');
|
||||
expect(result2.current.refreshInterval).toBe(10000);
|
||||
expect(result2.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
868
frontend/src/store/globalTime/__tests__/globalTimeStore.test.tsx
Normal file
868
frontend/src/store/globalTime/__tests__/globalTimeStore.test.tsx
Normal file
@@ -0,0 +1,868 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { ReactNode } from 'react';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
|
||||
import { createGlobalTimeStore, useGlobalTimeStore } from '../globalTimeStore';
|
||||
import { GlobalTimeContext } from '../GlobalTimeContext';
|
||||
import { useGlobalTime } from '../hooks';
|
||||
import { GlobalTimeSelectedTime, GlobalTimeState } from '../types';
|
||||
import { createCustomTimeRange, NANO_SECOND_MULTIPLIER } from '../utils';
|
||||
|
||||
/**
|
||||
* Creates an isolated store wrapper for testing.
|
||||
* Each test gets its own store instance, avoiding test pollution.
|
||||
*/
|
||||
function createIsolatedWrapper(
|
||||
initialState?: Partial<GlobalTimeState>,
|
||||
): ({ children }: { children: ReactNode }) => JSX.Element {
|
||||
const store = createGlobalTimeStore(initialState);
|
||||
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
{children}
|
||||
</GlobalTimeContext.Provider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
describe('globalTimeStore', () => {
|
||||
beforeEach(() => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
act(() => {
|
||||
result.current.setSelectedTime(DEFAULT_TIME_RANGE, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initial state', () => {
|
||||
it(`should have default selectedTime of ${DEFAULT_TIME_RANGE}`, () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.selectedTime).toBe(DEFAULT_TIME_RANGE);
|
||||
});
|
||||
|
||||
it('should have isRefreshEnabled as false by default', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should have refreshInterval as 0 by default', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.refreshInterval).toBe(0);
|
||||
});
|
||||
|
||||
it('should have lastRefreshTimestamp as 0 by default', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.lastRefreshTimestamp).toBe(0);
|
||||
});
|
||||
|
||||
it('should have lastComputedMinMax with default values', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
expect(result.current.lastComputedMinMax).toStrictEqual({
|
||||
minTime: 0,
|
||||
maxTime: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSelectedTime', () => {
|
||||
it('should update selectedTime', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
expect(result.current.selectedTime).toBe('15m');
|
||||
});
|
||||
|
||||
it('should update refreshInterval when provided', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
it('should keep existing refreshInterval when not provided', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('1h');
|
||||
});
|
||||
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
it('should enable refresh for relative time with refreshInterval > 0', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should disable refresh for relative time with refreshInterval = 0', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 0);
|
||||
});
|
||||
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should disable refresh for custom time range even with refreshInterval > 0', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
const customTime = createCustomTimeRange(1000000000, 2000000000);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime, 5000);
|
||||
});
|
||||
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
expect(result.current.refreshInterval).toBe(5000);
|
||||
});
|
||||
|
||||
it('should handle various relative time formats', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
const timeFormats: GlobalTimeSelectedTime[] = [
|
||||
'1m',
|
||||
'5m',
|
||||
'15m',
|
||||
'30m',
|
||||
'1h',
|
||||
'3h',
|
||||
'6h',
|
||||
'1d',
|
||||
'1w',
|
||||
];
|
||||
|
||||
timeFormats.forEach((time) => {
|
||||
act(() => {
|
||||
result.current.setSelectedTime(time, 10000);
|
||||
});
|
||||
|
||||
expect(result.current.selectedTime).toBe(time);
|
||||
expect(result.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should compute and store lastComputedMinMax when selectedTime changes', () => {
|
||||
const wrapper = createIsolatedWrapper({
|
||||
selectedTime: '15m',
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
// setSelectedTime computes values on init (createIsolatedWrapper uses createGlobalTimeStore)
|
||||
// But initial store state has minTime/maxTime as 0 until first setSelectedTime is called
|
||||
const initialMinMax = { ...result.current.lastComputedMinMax };
|
||||
|
||||
// Now switch to a custom time range
|
||||
const customTime = createCustomTimeRange(1000000000, 2000000000);
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime);
|
||||
});
|
||||
|
||||
// lastComputedMinMax should be updated to the custom range values
|
||||
expect(result.current.lastComputedMinMax).toStrictEqual({
|
||||
minTime: 1000000000,
|
||||
maxTime: 2000000000,
|
||||
});
|
||||
expect(result.current.lastComputedMinMax).not.toStrictEqual(initialMinMax);
|
||||
});
|
||||
|
||||
it('should return fresh custom time values after switching from relative time', () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z'));
|
||||
|
||||
const wrapper = createIsolatedWrapper({
|
||||
selectedTime: '15m',
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
// Compute and cache values for relative time
|
||||
act(() => {
|
||||
result.current.computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
const relativeMinMax = { ...result.current.lastComputedMinMax };
|
||||
|
||||
// Switch to custom time range
|
||||
const customMinTime = 5000000000;
|
||||
const customMaxTime = 6000000000;
|
||||
const customTime = createCustomTimeRange(customMinTime, customMaxTime);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime);
|
||||
});
|
||||
|
||||
// getMinMaxTime should return the custom time values, not cached relative values
|
||||
const returned = result.current.getMinMaxTime();
|
||||
|
||||
expect(returned.minTime).toBe(customMinTime);
|
||||
expect(returned.maxTime).toBe(customMaxTime);
|
||||
expect(returned).not.toStrictEqual(relativeMinMax);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMinMaxTime', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return min/max time for custom time range', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
const minTime = 1000000000;
|
||||
const maxTime = 2000000000;
|
||||
const customTime = createCustomTimeRange(minTime, maxTime);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime);
|
||||
});
|
||||
|
||||
const { minTime: resultMin, maxTime: resultMax } =
|
||||
result.current.getMinMaxTime();
|
||||
expect(resultMin).toBe(minTime);
|
||||
expect(resultMax).toBe(maxTime);
|
||||
});
|
||||
|
||||
it('should NOT round custom time range values to minute boundaries', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
// Use timestamps that are NOT on minute boundaries (12:30:45.123)
|
||||
// If rounding occurred, these would change to 12:30:00.000
|
||||
const minTimeWithSeconds =
|
||||
new Date('2024-01-15T12:15:45.123Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
const maxTimeWithSeconds =
|
||||
new Date('2024-01-15T12:30:45.123Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
// What the values would be if rounded down to minute boundary
|
||||
const minTimeRounded =
|
||||
new Date('2024-01-15T12:15:00.000Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
const maxTimeRounded =
|
||||
new Date('2024-01-15T12:30:00.000Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
const customTime = createCustomTimeRange(
|
||||
minTimeWithSeconds,
|
||||
maxTimeWithSeconds,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime);
|
||||
});
|
||||
|
||||
const { minTime, maxTime } = result.current.getMinMaxTime();
|
||||
|
||||
// Should return exact values, NOT rounded values
|
||||
expect(minTime).toBe(minTimeWithSeconds);
|
||||
expect(maxTime).toBe(maxTimeWithSeconds);
|
||||
expect(minTime).not.toBe(minTimeRounded);
|
||||
expect(maxTime).not.toBe(maxTimeRounded);
|
||||
});
|
||||
|
||||
it('should compute fresh min/max time for relative time', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
const { minTime, maxTime } = result.current.getMinMaxTime();
|
||||
const now = Date.now() * NANO_SECOND_MULTIPLIER;
|
||||
const fifteenMinutesNs = 15 * 60 * 1000 * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
expect(maxTime).toBe(now);
|
||||
expect(minTime).toBe(now - fifteenMinutesNs);
|
||||
});
|
||||
|
||||
it('should return same values on subsequent calls when refresh disabled (under minute boundary)', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 0); // refresh disabled
|
||||
});
|
||||
|
||||
const first = result.current.getMinMaxTime();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(59000);
|
||||
});
|
||||
|
||||
const second = result.current.getMinMaxTime();
|
||||
|
||||
// With refresh disabled, should return cached lastComputedMinMax
|
||||
expect(second.maxTime).toBe(first.maxTime);
|
||||
expect(second.minTime).toBe(first.minTime);
|
||||
});
|
||||
|
||||
it('should return different values on subsequent calls when refresh disabled after minute boundary', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 0); // refresh disabled
|
||||
});
|
||||
|
||||
const first = result.current.getMinMaxTime();
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60000);
|
||||
});
|
||||
|
||||
// Without refresh enabled, getMinMaxTime returns cached values
|
||||
// Need to call computeAndStoreMinMax to get new values
|
||||
const second = result.current.getMinMaxTime();
|
||||
expect(second.maxTime).toBe(first.maxTime);
|
||||
|
||||
// After computing, values should update
|
||||
act(() => {
|
||||
result.current.computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
const third = result.current.getMinMaxTime();
|
||||
expect(third.maxTime).toBe(first.maxTime + 60000 * NANO_SECOND_MULTIPLIER);
|
||||
expect(third.minTime).toBe(first.minTime + 60000 * NANO_SECOND_MULTIPLIER);
|
||||
});
|
||||
|
||||
it('should return stored lastComputedMinMax when available', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
result.current.computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
const stored = { ...result.current.lastComputedMinMax };
|
||||
|
||||
// Advance time by 5 seconds
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
|
||||
// getMinMaxTime should return stored values, not fresh computation
|
||||
const returned = result.current.getMinMaxTime();
|
||||
expect(returned).toStrictEqual(stored);
|
||||
});
|
||||
|
||||
describe('with isRefreshEnabled (isolated store)', () => {
|
||||
it('should compute fresh values when isRefreshEnabled is true (5s rounding)', () => {
|
||||
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z')); // Start at 5s boundary
|
||||
|
||||
const wrapper = createIsolatedWrapper({
|
||||
selectedTime: '15m',
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
// getMinMaxTime computes 5s-rounded values when refresh enabled
|
||||
const initialMinMax = result.current.getMinMaxTime();
|
||||
|
||||
// Advance time by 5 seconds to cross 5s boundary
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
|
||||
// getMinMaxTime should return fresh values, not cached
|
||||
const freshValues = result.current.getMinMaxTime();
|
||||
|
||||
expect(freshValues.maxTime).toBe(
|
||||
initialMinMax.maxTime + 5000 * NANO_SECOND_MULTIPLIER,
|
||||
);
|
||||
expect(freshValues.minTime).toBe(
|
||||
initialMinMax.minTime + 5000 * NANO_SECOND_MULTIPLIER,
|
||||
);
|
||||
});
|
||||
|
||||
it('should update lastComputedMinMax when values change (5s rounding)', () => {
|
||||
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z')); // Start at 5s boundary
|
||||
|
||||
const wrapper = createIsolatedWrapper({
|
||||
selectedTime: '15m',
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
// Get initial values (uses 5s rounding when refresh enabled)
|
||||
const initialMinMax = result.current.getMinMaxTime();
|
||||
|
||||
// Advance time by 5 seconds to cross 5s boundary
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
|
||||
// Call getMinMaxTime - should update lastComputedMinMax
|
||||
act(() => {
|
||||
result.current.getMinMaxTime();
|
||||
});
|
||||
|
||||
expect(result.current.lastComputedMinMax.maxTime).toBe(
|
||||
initialMinMax.maxTime + 5000 * NANO_SECOND_MULTIPLIER,
|
||||
);
|
||||
expect(result.current.lastComputedMinMax.minTime).toBe(
|
||||
initialMinMax.minTime + 5000 * NANO_SECOND_MULTIPLIER,
|
||||
);
|
||||
});
|
||||
|
||||
it('should update lastRefreshTimestamp when values change', () => {
|
||||
const wrapper = createIsolatedWrapper({
|
||||
selectedTime: '15m',
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
const initialTimestamp = result.current.lastRefreshTimestamp;
|
||||
|
||||
// Advance time past minute boundary
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60000);
|
||||
});
|
||||
|
||||
// Call getMinMaxTime - should update timestamp
|
||||
act(() => {
|
||||
result.current.getMinMaxTime();
|
||||
});
|
||||
|
||||
expect(result.current.lastRefreshTimestamp).toBeGreaterThan(
|
||||
initialTimestamp,
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT update lastComputedMinMax when values have not changed (same 5s window)', () => {
|
||||
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z')); // Start at 5s boundary
|
||||
|
||||
const wrapper = createIsolatedWrapper({
|
||||
selectedTime: '15m',
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
// Get initial values (triggers computation for 5s-rounded values)
|
||||
result.current.getMinMaxTime();
|
||||
|
||||
const initialMinMax = { ...result.current.lastComputedMinMax };
|
||||
const initialTimestamp = result.current.lastRefreshTimestamp;
|
||||
|
||||
// Advance time but stay within same 5-second window
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(4000);
|
||||
});
|
||||
|
||||
// Call getMinMaxTime - should NOT update store (same 5s boundary)
|
||||
act(() => {
|
||||
result.current.getMinMaxTime();
|
||||
});
|
||||
|
||||
// Values should be unchanged (no unnecessary re-renders)
|
||||
expect(result.current.lastComputedMinMax).toStrictEqual(initialMinMax);
|
||||
expect(result.current.lastRefreshTimestamp).toBe(initialTimestamp);
|
||||
});
|
||||
|
||||
it('should return cached values when isRefreshEnabled is false', () => {
|
||||
const wrapper = createIsolatedWrapper({
|
||||
selectedTime: '15m',
|
||||
refreshInterval: 0, // Refresh disabled
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
act(() => {
|
||||
result.current.computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
const storedMinMax = { ...result.current.lastComputedMinMax };
|
||||
|
||||
// Advance time past minute boundary
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60000);
|
||||
});
|
||||
|
||||
// getMinMaxTime should return cached values since refresh is disabled
|
||||
const returned = result.current.getMinMaxTime();
|
||||
|
||||
expect(returned).toStrictEqual(storedMinMax);
|
||||
expect(result.current.lastComputedMinMax).toStrictEqual(storedMinMax);
|
||||
});
|
||||
|
||||
it('should return same values for custom time range regardless of time passing', () => {
|
||||
const minTime = 1000000000;
|
||||
const maxTime = 2000000000;
|
||||
const customTime = createCustomTimeRange(minTime, maxTime);
|
||||
|
||||
const wrapper = createIsolatedWrapper({
|
||||
selectedTime: customTime,
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
// isRefreshEnabled should be false for custom time ranges
|
||||
expect(result.current.isRefreshEnabled).toBe(false);
|
||||
|
||||
// Custom time ranges always return the fixed values, not relative to "now"
|
||||
const first = result.current.getMinMaxTime();
|
||||
expect(first.minTime).toBe(minTime);
|
||||
expect(first.maxTime).toBe(maxTime);
|
||||
|
||||
// Advance time past minute boundary
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60000);
|
||||
});
|
||||
|
||||
// Should still return the same fixed values (custom range doesn't drift)
|
||||
const second = result.current.getMinMaxTime();
|
||||
expect(second.minTime).toBe(minTime);
|
||||
expect(second.maxTime).toBe(maxTime);
|
||||
});
|
||||
|
||||
it('should handle multiple consecutive refetch intervals correctly (5s rounding)', () => {
|
||||
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z')); // Start at 5s boundary
|
||||
|
||||
const wrapper = createIsolatedWrapper({
|
||||
selectedTime: '15m',
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
// Get initial values
|
||||
const initialMinMax = result.current.getMinMaxTime();
|
||||
|
||||
// Simulate 3 refetch intervals crossing 5-second boundaries
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.getMinMaxTime();
|
||||
});
|
||||
|
||||
expect(result.current.lastComputedMinMax.maxTime).toBe(
|
||||
initialMinMax.maxTime + i * 5000 * NANO_SECOND_MULTIPLIER,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeAndStoreMinMax', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:30:45.123Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should compute and store min/max values', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
// maxTime should be the current time (no rounding when refresh disabled)
|
||||
const expectedMaxTime =
|
||||
new Date('2024-01-15T12:30:45.123Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
const fifteenMinutesNs = 15 * 60 * 1000 * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
expect(result.current.lastComputedMinMax.maxTime).toBe(expectedMaxTime);
|
||||
expect(result.current.lastComputedMinMax.minTime).toBe(
|
||||
expectedMaxTime - fifteenMinutesNs,
|
||||
);
|
||||
});
|
||||
|
||||
it('should update lastRefreshTimestamp', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
const beforeTimestamp = Date.now();
|
||||
|
||||
act(() => {
|
||||
result.current.computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
expect(result.current.lastRefreshTimestamp).toBeGreaterThanOrEqual(
|
||||
beforeTimestamp,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the computed values', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
let returnedValue: { minTime: number; maxTime: number } | undefined;
|
||||
act(() => {
|
||||
returnedValue = result.current.computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
expect(returnedValue).toStrictEqual(result.current.lastComputedMinMax);
|
||||
});
|
||||
|
||||
it('should NOT round custom time range values to minute boundaries', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
// Use timestamps that are NOT on minute boundaries (12:30:45.123)
|
||||
// If rounding occurred, these would change to 12:30:00.000
|
||||
const minTimeWithSeconds =
|
||||
new Date('2024-01-15T12:15:45.123Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
const maxTimeWithSeconds =
|
||||
new Date('2024-01-15T12:30:45.123Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
// What the values would be if rounded down to minute boundary
|
||||
const minTimeRounded =
|
||||
new Date('2024-01-15T12:15:00.000Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
const maxTimeRounded =
|
||||
new Date('2024-01-15T12:30:00.000Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
const customTime = createCustomTimeRange(
|
||||
minTimeWithSeconds,
|
||||
maxTimeWithSeconds,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime);
|
||||
});
|
||||
|
||||
let returnedValue: { minTime: number; maxTime: number } | undefined;
|
||||
act(() => {
|
||||
returnedValue = result.current.computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
// Should return exact values, NOT rounded values
|
||||
expect(returnedValue?.minTime).toBe(minTimeWithSeconds);
|
||||
expect(returnedValue?.maxTime).toBe(maxTimeWithSeconds);
|
||||
expect(returnedValue?.minTime).not.toBe(minTimeRounded);
|
||||
expect(returnedValue?.maxTime).not.toBe(maxTimeRounded);
|
||||
|
||||
// lastComputedMinMax should also have exact values
|
||||
expect(result.current.lastComputedMinMax.minTime).toBe(minTimeWithSeconds);
|
||||
expect(result.current.lastComputedMinMax.maxTime).toBe(maxTimeWithSeconds);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateRefreshTimestamp', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:30:45.123Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should update lastRefreshTimestamp to current time', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.updateRefreshTimestamp();
|
||||
});
|
||||
|
||||
expect(result.current.lastRefreshTimestamp).toBe(Date.now());
|
||||
});
|
||||
|
||||
it('should not modify lastComputedMinMax', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result.current.computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
const beforeMinMax = { ...result.current.lastComputedMinMax };
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
result.current.updateRefreshTimestamp();
|
||||
});
|
||||
|
||||
expect(result.current.lastComputedMinMax).toStrictEqual(beforeMinMax);
|
||||
});
|
||||
});
|
||||
|
||||
describe('store isolation', () => {
|
||||
it('should share state between multiple hook instances', () => {
|
||||
const { result: result1 } = renderHook(() => useGlobalTimeStore());
|
||||
const { result: result2 } = renderHook(() => useGlobalTimeStore());
|
||||
|
||||
act(() => {
|
||||
result1.current.setSelectedTime('1h', 10000);
|
||||
});
|
||||
|
||||
expect(result2.current.selectedTime).toBe('1h');
|
||||
expect(result2.current.refreshInterval).toBe(10000);
|
||||
expect(result2.current.isRefreshEnabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSelectedTime (min/max computation)', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should compute and store min/max for relative time on setSelectedTime', () => {
|
||||
const wrapper = createIsolatedWrapper();
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
// Initial state has 0 values
|
||||
expect(result.current.lastComputedMinMax.maxTime).toBe(0);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
// Should have computed values immediately
|
||||
const expectedMaxTime =
|
||||
new Date('2024-01-15T12:00:00.000Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
const fifteenMinutesNs = 15 * 60 * 1000 * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
expect(result.current.lastComputedMinMax.maxTime).toBe(expectedMaxTime);
|
||||
expect(result.current.lastComputedMinMax.minTime).toBe(
|
||||
expectedMaxTime - fifteenMinutesNs,
|
||||
);
|
||||
});
|
||||
|
||||
it('should compute and store min/max for custom time on setSelectedTime', () => {
|
||||
const wrapper = createIsolatedWrapper();
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
const minTime = 1000000000;
|
||||
const maxTime = 2000000000;
|
||||
const customTime = createCustomTimeRange(minTime, maxTime);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime(customTime);
|
||||
});
|
||||
|
||||
expect(result.current.lastComputedMinMax.minTime).toBe(minTime);
|
||||
expect(result.current.lastComputedMinMax.maxTime).toBe(maxTime);
|
||||
});
|
||||
|
||||
it('should update lastRefreshTimestamp on setSelectedTime', () => {
|
||||
const wrapper = createIsolatedWrapper();
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
expect(result.current.lastRefreshTimestamp).toBe(0);
|
||||
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m');
|
||||
});
|
||||
|
||||
expect(result.current.lastRefreshTimestamp).toBe(Date.now());
|
||||
});
|
||||
|
||||
it('should skip update when same selectedTime and refreshInterval', () => {
|
||||
const wrapper = createIsolatedWrapper({
|
||||
selectedTime: '15m',
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
// Set initial values
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
const initialTimestamp = result.current.lastRefreshTimestamp;
|
||||
|
||||
// Advance time
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
// Try to set same values again
|
||||
act(() => {
|
||||
result.current.setSelectedTime('15m', 5000);
|
||||
});
|
||||
|
||||
// Should not have updated timestamp (no state change)
|
||||
expect(result.current.lastRefreshTimestamp).toBe(initialTimestamp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeAndStoreMinMax (refresh behavior)', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should skip computation and return lastComputedMinMax when refresh is enabled', () => {
|
||||
const wrapper = createIsolatedWrapper({
|
||||
selectedTime: '15m',
|
||||
refreshInterval: 5000,
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
// Get initial values via getMinMaxTime (which computes for refresh enabled)
|
||||
const initialMinMax = result.current.getMinMaxTime();
|
||||
|
||||
// Advance time
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60000);
|
||||
});
|
||||
|
||||
// computeAndStoreMinMax should skip computation when refresh is enabled
|
||||
let returnedValue: { minTime: number; maxTime: number } | undefined;
|
||||
act(() => {
|
||||
returnedValue = result.current.computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
// Should return the current lastComputedMinMax, not fresh computation
|
||||
expect(returnedValue).toStrictEqual(initialMinMax);
|
||||
});
|
||||
|
||||
it('should compute fresh values when refresh is disabled', () => {
|
||||
const wrapper = createIsolatedWrapper({
|
||||
selectedTime: '15m',
|
||||
refreshInterval: 0, // Disabled
|
||||
});
|
||||
const { result } = renderHook(() => useGlobalTime(), { wrapper });
|
||||
|
||||
// Get initial values
|
||||
act(() => {
|
||||
result.current.computeAndStoreMinMax();
|
||||
});
|
||||
const initialMinMax = { ...result.current.lastComputedMinMax };
|
||||
|
||||
// Advance time past minute boundary
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60000);
|
||||
});
|
||||
|
||||
// computeAndStoreMinMax should compute fresh values
|
||||
let returnedValue: { minTime: number; maxTime: number } | undefined;
|
||||
act(() => {
|
||||
returnedValue = result.current.computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
// Should return new values
|
||||
expect(returnedValue?.maxTime).toBe(
|
||||
initialMinMax.maxTime + 60000 * NANO_SECOND_MULTIPLIER,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
190
frontend/src/store/globalTime/__tests__/hooks.test.tsx
Normal file
190
frontend/src/store/globalTime/__tests__/hooks.test.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { createGlobalTimeStore } from '../globalTimeStore';
|
||||
import { GlobalTimeContext } from '../GlobalTimeContext';
|
||||
import {
|
||||
useGlobalTime,
|
||||
useGlobalTimeStoreApi,
|
||||
useIsCustomTimeRange,
|
||||
useLastComputedMinMax,
|
||||
} from '../hooks';
|
||||
import { useComputedMinMaxSync } from '../useComputedMinMaxSync';
|
||||
import { createCustomTimeRange } from '../utils';
|
||||
|
||||
describe('useGlobalTime', () => {
|
||||
it('should return full store state without selector', () => {
|
||||
const { result } = renderHook(() => useGlobalTime());
|
||||
|
||||
expect(result.current.selectedTime).toBeDefined();
|
||||
expect(result.current.setSelectedTime).toBeInstanceOf(Function);
|
||||
});
|
||||
|
||||
it('should return selected value with selector', () => {
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime));
|
||||
|
||||
expect(typeof result.current).toBe('string');
|
||||
});
|
||||
|
||||
it('should use context store when provided', () => {
|
||||
const contextStore = createGlobalTimeStore({ selectedTime: '1h' });
|
||||
|
||||
const wrapper = ({ children }: { children: ReactNode }): JSX.Element => (
|
||||
<GlobalTimeContext.Provider value={contextStore}>
|
||||
{children}
|
||||
</GlobalTimeContext.Provider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useGlobalTime((s) => s.selectedTime), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current).toBe('1h');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useIsCustomTimeRange', () => {
|
||||
it('should return false for relative time', () => {
|
||||
const { result } = renderHook(() => useIsCustomTimeRange());
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true for custom time range', () => {
|
||||
const customTime = createCustomTimeRange(1000000000, 2000000000);
|
||||
const contextStore = createGlobalTimeStore({ selectedTime: customTime });
|
||||
|
||||
const { result } = renderHook(() => useIsCustomTimeRange(), {
|
||||
wrapper: ({ children }: { children: ReactNode }): JSX.Element => (
|
||||
<GlobalTimeContext.Provider value={contextStore}>
|
||||
{children}
|
||||
</GlobalTimeContext.Provider>
|
||||
),
|
||||
});
|
||||
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useGlobalTimeStoreApi', () => {
|
||||
it('should return store API', () => {
|
||||
const { result } = renderHook(() => useGlobalTimeStoreApi());
|
||||
|
||||
expect(result.current.getState).toBeInstanceOf(Function);
|
||||
expect(result.current.subscribe).toBeInstanceOf(Function);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useLastComputedMinMax', () => {
|
||||
it('should return lastComputedMinMax from store', () => {
|
||||
const contextStore = createGlobalTimeStore({ selectedTime: '15m' });
|
||||
|
||||
// Compute the min/max first
|
||||
contextStore.getState().computeAndStoreMinMax();
|
||||
|
||||
const { result } = renderHook(() => useLastComputedMinMax(), {
|
||||
wrapper: ({ children }: { children: ReactNode }): JSX.Element => (
|
||||
<GlobalTimeContext.Provider value={contextStore}>
|
||||
{children}
|
||||
</GlobalTimeContext.Provider>
|
||||
),
|
||||
});
|
||||
|
||||
expect(result.current).toStrictEqual(
|
||||
contextStore.getState().lastComputedMinMax,
|
||||
);
|
||||
});
|
||||
|
||||
it('should update when store changes', () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:30:45.123Z'));
|
||||
|
||||
const contextStore = createGlobalTimeStore({ selectedTime: '15m' });
|
||||
contextStore.getState().computeAndStoreMinMax();
|
||||
|
||||
const { result } = renderHook(() => useLastComputedMinMax(), {
|
||||
wrapper: ({ children }: { children: ReactNode }): JSX.Element => (
|
||||
<GlobalTimeContext.Provider value={contextStore}>
|
||||
{children}
|
||||
</GlobalTimeContext.Provider>
|
||||
),
|
||||
});
|
||||
|
||||
const firstValue = { ...result.current };
|
||||
|
||||
// Change time and recompute
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60000); // Advance 1 minute
|
||||
contextStore.getState().computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
expect(result.current).not.toStrictEqual(firstValue);
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useComputedMinMaxSync', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:30:45.123Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should compute min/max on mount when store has zero values', () => {
|
||||
const contextStore = createGlobalTimeStore({ selectedTime: '15m' });
|
||||
|
||||
expect(contextStore.getState().lastComputedMinMax).toStrictEqual({
|
||||
minTime: 0,
|
||||
maxTime: 0,
|
||||
});
|
||||
|
||||
renderHook(() => useComputedMinMaxSync(contextStore));
|
||||
|
||||
// Should have computed values now
|
||||
expect(contextStore.getState().lastComputedMinMax.maxTime).toBeGreaterThan(0);
|
||||
expect(contextStore.getState().lastComputedMinMax.minTime).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should NOT recompute when store already has values', () => {
|
||||
const contextStore = createGlobalTimeStore({ selectedTime: '15m' });
|
||||
|
||||
contextStore.getState().computeAndStoreMinMax();
|
||||
const initialMinMax = { ...contextStore.getState().lastComputedMinMax };
|
||||
const initialTimestamp = contextStore.getState().lastRefreshTimestamp;
|
||||
|
||||
jest.advanceTimersByTime(60000);
|
||||
|
||||
renderHook(() => useComputedMinMaxSync(contextStore));
|
||||
|
||||
// Should NOT have recomputed - values should be unchanged
|
||||
expect(contextStore.getState().lastComputedMinMax).toStrictEqual(
|
||||
initialMinMax,
|
||||
);
|
||||
expect(contextStore.getState().lastRefreshTimestamp).toBe(initialTimestamp);
|
||||
});
|
||||
|
||||
it('should only compute on mount, not on re-renders', () => {
|
||||
const contextStore = createGlobalTimeStore({ selectedTime: '15m' });
|
||||
|
||||
const { rerender } = renderHook(() => useComputedMinMaxSync(contextStore));
|
||||
|
||||
const afterMountMinMax = { ...contextStore.getState().lastComputedMinMax };
|
||||
const afterMountTimestamp = contextStore.getState().lastRefreshTimestamp;
|
||||
|
||||
jest.advanceTimersByTime(60000);
|
||||
|
||||
rerender();
|
||||
|
||||
// Should NOT have recomputed on re-render
|
||||
expect(contextStore.getState().lastComputedMinMax).toStrictEqual(
|
||||
afterMountMinMax,
|
||||
);
|
||||
expect(contextStore.getState().lastRefreshTimestamp).toBe(
|
||||
afterMountTimestamp,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,381 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
|
||||
import { GlobalTimeProvider } from '../GlobalTimeContext';
|
||||
import { useGlobalTime } from '../hooks';
|
||||
import { GlobalTimeProviderOptions } from '../types';
|
||||
import { useGlobalTimeQueryInvalidate } from '../useGlobalTimeQueryInvalidate';
|
||||
import { createCustomTimeRange, NANO_SECOND_MULTIPLIER } from '../utils';
|
||||
|
||||
const createTestQueryClient = (): QueryClient =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
cacheTime: Infinity,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createWrapper = (
|
||||
providerProps: GlobalTimeProviderOptions,
|
||||
queryClient: QueryClient,
|
||||
) => {
|
||||
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter>
|
||||
<GlobalTimeProvider {...providerProps}>{children}</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
describe('useGlobalTimeQueryInvalidate', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = createTestQueryClient();
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:30:45.123Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
it('should return a function', () => {
|
||||
const wrapper = createWrapper({ initialTime: '15m' }, queryClient);
|
||||
const { result } = renderHook(() => useGlobalTimeQueryInvalidate(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(typeof result.current).toBe('function');
|
||||
});
|
||||
|
||||
it('should call computeAndStoreMinMax before invalidating queries (refresh disabled)', async () => {
|
||||
const wrapper = createWrapper(
|
||||
{ initialTime: '15m', refreshInterval: 0 }, // refresh disabled so computeAndStoreMinMax computes fresh values
|
||||
queryClient,
|
||||
);
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
invalidate: useGlobalTimeQueryInvalidate(),
|
||||
globalTime: useGlobalTime(),
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Initial computation - need to call computeAndStoreMinMax first
|
||||
act(() => {
|
||||
result.current.globalTime.computeAndStoreMinMax();
|
||||
});
|
||||
const initialMinMax = { ...result.current.globalTime.lastComputedMinMax };
|
||||
|
||||
// Advance time past minute boundary
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(60000);
|
||||
});
|
||||
|
||||
// Call invalidate - should compute fresh values when refresh is disabled
|
||||
await act(async () => {
|
||||
await result.current.invalidate();
|
||||
});
|
||||
|
||||
// lastComputedMinMax should have been updated
|
||||
expect(result.current.globalTime.lastComputedMinMax.maxTime).toBe(
|
||||
initialMinMax.maxTime + 60000 * NANO_SECOND_MULTIPLIER,
|
||||
);
|
||||
});
|
||||
|
||||
it('should invalidate queries with AUTO_REFRESH_QUERY key', async () => {
|
||||
const mockQueryFn = jest.fn().mockResolvedValue({ data: 'test' });
|
||||
|
||||
const wrapper = createWrapper({ initialTime: '15m' }, queryClient);
|
||||
|
||||
// Set up a query with AUTO_REFRESH_QUERY key
|
||||
const { result: queryResult } = renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'test-query'],
|
||||
queryFn: mockQueryFn,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Wait for initial query to complete
|
||||
await waitFor(() => {
|
||||
expect(queryResult.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(mockQueryFn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Now render the invalidate hook and call it
|
||||
const { result: invalidateResult } = renderHook(
|
||||
() => useGlobalTimeQueryInvalidate(),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await invalidateResult.current();
|
||||
});
|
||||
|
||||
// Query should have been refetched
|
||||
await waitFor(() => {
|
||||
expect(mockQueryFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT invalidate queries without AUTO_REFRESH_QUERY key', async () => {
|
||||
const autoRefreshQueryFn = jest.fn().mockResolvedValue({ data: 'auto' });
|
||||
const regularQueryFn = jest.fn().mockResolvedValue({ data: 'regular' });
|
||||
|
||||
const wrapper = createWrapper({ initialTime: '15m' }, queryClient);
|
||||
|
||||
// Set up both types of queries
|
||||
const { result: autoRefreshQuery } = renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'auto-query'],
|
||||
queryFn: autoRefreshQueryFn,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
const { result: regularQuery } = renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: ['regular-query'],
|
||||
queryFn: regularQueryFn,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Wait for initial queries to complete
|
||||
await waitFor(() => {
|
||||
expect(autoRefreshQuery.current.isSuccess).toBe(true);
|
||||
expect(regularQuery.current.isSuccess).toBe(true);
|
||||
});
|
||||
|
||||
expect(autoRefreshQueryFn).toHaveBeenCalledTimes(1);
|
||||
expect(regularQueryFn).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Call invalidate
|
||||
const { result: invalidateResult } = renderHook(
|
||||
() => useGlobalTimeQueryInvalidate(),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await invalidateResult.current();
|
||||
});
|
||||
|
||||
// Only auto-refresh query should be refetched
|
||||
await waitFor(() => {
|
||||
expect(autoRefreshQueryFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
// Regular query should NOT be refetched
|
||||
expect(regularQueryFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should use exact custom time values (not rounded) when invalidating', async () => {
|
||||
// Use timestamps that are NOT on minute boundaries
|
||||
const minTimeWithSeconds =
|
||||
new Date('2024-01-15T12:15:45.123Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
const maxTimeWithSeconds =
|
||||
new Date('2024-01-15T12:30:45.123Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
const customTime = createCustomTimeRange(
|
||||
minTimeWithSeconds,
|
||||
maxTimeWithSeconds,
|
||||
);
|
||||
|
||||
const wrapper = createWrapper({ initialTime: customTime }, queryClient);
|
||||
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
invalidate: useGlobalTimeQueryInvalidate(),
|
||||
globalTime: useGlobalTime(),
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Call invalidate
|
||||
await act(async () => {
|
||||
await result.current.invalidate();
|
||||
});
|
||||
|
||||
// Verify custom time values are NOT rounded
|
||||
expect(result.current.globalTime.lastComputedMinMax.minTime).toBe(
|
||||
minTimeWithSeconds,
|
||||
);
|
||||
expect(result.current.globalTime.lastComputedMinMax.maxTime).toBe(
|
||||
maxTimeWithSeconds,
|
||||
);
|
||||
});
|
||||
|
||||
it('should invalidate multiple AUTO_REFRESH_QUERY queries at once', async () => {
|
||||
const queryFn1 = jest.fn().mockResolvedValue({ data: 'query1' });
|
||||
const queryFn2 = jest.fn().mockResolvedValue({ data: 'query2' });
|
||||
const queryFn3 = jest.fn().mockResolvedValue({ data: 'query3' });
|
||||
|
||||
const wrapper = createWrapper({ initialTime: '15m' }, queryClient);
|
||||
|
||||
// Set up multiple auto-refresh queries
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'query1'],
|
||||
queryFn: queryFn1,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'query2'],
|
||||
queryFn: queryFn2,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'query3'],
|
||||
queryFn: queryFn3,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Wait for initial queries
|
||||
await waitFor(() => {
|
||||
expect(queryFn1).toHaveBeenCalledTimes(1);
|
||||
expect(queryFn2).toHaveBeenCalledTimes(1);
|
||||
expect(queryFn3).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Call invalidate
|
||||
const { result } = renderHook(() => useGlobalTimeQueryInvalidate(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current();
|
||||
});
|
||||
|
||||
// All queries should be refetched
|
||||
await waitFor(() => {
|
||||
expect(queryFn1).toHaveBeenCalledTimes(2);
|
||||
expect(queryFn2).toHaveBeenCalledTimes(2);
|
||||
expect(queryFn3).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scoped invalidation with store name', () => {
|
||||
it('should only invalidate queries matching store name', async () => {
|
||||
const namedQueryFn = jest.fn().mockResolvedValue({ data: 'named' });
|
||||
const unnamedQueryFn = jest.fn().mockResolvedValue({ data: 'unnamed' });
|
||||
|
||||
const wrapper = createWrapper(
|
||||
{ name: 'drawer', initialTime: '15m' },
|
||||
queryClient,
|
||||
);
|
||||
|
||||
// Query with matching name
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'drawer', 'named-query'],
|
||||
queryFn: namedQueryFn,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Query without name (different store)
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'unnamed-query'],
|
||||
queryFn: unnamedQueryFn,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(namedQueryFn).toHaveBeenCalledTimes(1);
|
||||
expect(unnamedQueryFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// Call invalidate
|
||||
const { result } = renderHook(() => useGlobalTimeQueryInvalidate(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current();
|
||||
});
|
||||
|
||||
// Only named query should be refetched
|
||||
await waitFor(() => {
|
||||
expect(namedQueryFn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
// Unnamed query should NOT be refetched
|
||||
expect(unnamedQueryFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should invalidate all queries for unnamed store (backward compatible)', async () => {
|
||||
const queryFn1 = jest.fn().mockResolvedValue({ data: 'query1' });
|
||||
const queryFn2 = jest.fn().mockResolvedValue({ data: 'query2' });
|
||||
|
||||
// Unnamed store (no name prop)
|
||||
const wrapper = createWrapper({ initialTime: '15m' }, queryClient);
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'query1'],
|
||||
queryFn: queryFn1,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'query2'],
|
||||
queryFn: queryFn2,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(queryFn1).toHaveBeenCalledTimes(1);
|
||||
expect(queryFn2).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useGlobalTimeQueryInvalidate(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await result.current();
|
||||
});
|
||||
|
||||
// Both should be refetched
|
||||
await waitFor(() => {
|
||||
expect(queryFn1).toHaveBeenCalledTimes(2);
|
||||
expect(queryFn2).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,323 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
|
||||
import { GlobalTimeProvider } from '../GlobalTimeContext';
|
||||
import { GlobalTimeProviderOptions } from '../types';
|
||||
import { useIsGlobalTimeQueryRefreshing } from '../useIsGlobalTimeQueryRefreshing';
|
||||
|
||||
const createTestQueryClient = (): QueryClient =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createWrapper = (
|
||||
queryClient: QueryClient,
|
||||
): (({ children }: { children: ReactNode }) => JSX.Element) => {
|
||||
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
const createProviderWrapper = (
|
||||
providerProps: GlobalTimeProviderOptions,
|
||||
queryClient: QueryClient,
|
||||
): (({ children }: { children: ReactNode }) => JSX.Element) => {
|
||||
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<NuqsTestingAdapter>
|
||||
<GlobalTimeProvider {...providerProps}>{children}</GlobalTimeProvider>
|
||||
</NuqsTestingAdapter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
describe('useIsGlobalTimeQueryRefreshing', () => {
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = createTestQueryClient();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
it('should return false when no queries are fetching', () => {
|
||||
const wrapper = createWrapper(queryClient);
|
||||
const { result } = renderHook(() => useIsGlobalTimeQueryRefreshing(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when AUTO_REFRESH_QUERY is fetching', async () => {
|
||||
let resolveQuery: (value: unknown) => void;
|
||||
const queryPromise = new Promise((resolve) => {
|
||||
resolveQuery = resolve;
|
||||
});
|
||||
|
||||
const wrapper = createWrapper(queryClient);
|
||||
|
||||
// Start the auto-refresh query
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'test'],
|
||||
queryFn: () => queryPromise,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Check if refreshing hook detects it
|
||||
const { result } = renderHook(() => useIsGlobalTimeQueryRefreshing(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Should be true while fetching
|
||||
expect(result.current).toBe(true);
|
||||
|
||||
// Resolve the query
|
||||
act(() => {
|
||||
resolveQuery({ data: 'done' });
|
||||
});
|
||||
|
||||
// Should be false after fetching completes
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false when non-AUTO_REFRESH_QUERY is fetching', async () => {
|
||||
let resolveQuery: (value: unknown) => void;
|
||||
const queryPromise = new Promise((resolve) => {
|
||||
resolveQuery = resolve;
|
||||
});
|
||||
|
||||
const wrapper = createWrapper(queryClient);
|
||||
|
||||
// Start a regular query (not auto-refresh)
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: ['regular-query'],
|
||||
queryFn: () => queryPromise,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Check if refreshing hook detects it
|
||||
const { result } = renderHook(() => useIsGlobalTimeQueryRefreshing(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Should be false - not an auto-refresh query
|
||||
expect(result.current).toBe(false);
|
||||
|
||||
// Cleanup
|
||||
act(() => {
|
||||
resolveQuery({ data: 'done' });
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true when multiple AUTO_REFRESH_QUERY queries are fetching', async () => {
|
||||
let resolveQuery1: (value: unknown) => void;
|
||||
let resolveQuery2: (value: unknown) => void;
|
||||
const queryPromise1 = new Promise((resolve) => {
|
||||
resolveQuery1 = resolve;
|
||||
});
|
||||
const queryPromise2 = new Promise((resolve) => {
|
||||
resolveQuery2 = resolve;
|
||||
});
|
||||
|
||||
const wrapper = createWrapper(queryClient);
|
||||
|
||||
// Start multiple auto-refresh queries
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'query1'],
|
||||
queryFn: () => queryPromise1,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'query2'],
|
||||
queryFn: () => queryPromise2,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useIsGlobalTimeQueryRefreshing(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Should be true while fetching
|
||||
expect(result.current).toBe(true);
|
||||
|
||||
// Resolve first query
|
||||
act(() => {
|
||||
resolveQuery1({ data: 'done1' });
|
||||
});
|
||||
|
||||
// Should still be true (second query still fetching)
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
// Resolve second query
|
||||
act(() => {
|
||||
resolveQuery2({ data: 'done2' });
|
||||
});
|
||||
|
||||
// Should be false after all complete
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should only track AUTO_REFRESH_QUERY, not other queries', async () => {
|
||||
let resolveAutoRefresh: (value: unknown) => void;
|
||||
let resolveRegular: (value: unknown) => void;
|
||||
const autoRefreshPromise = new Promise((resolve) => {
|
||||
resolveAutoRefresh = resolve;
|
||||
});
|
||||
const regularPromise = new Promise((resolve) => {
|
||||
resolveRegular = resolve;
|
||||
});
|
||||
|
||||
const wrapper = createWrapper(queryClient);
|
||||
|
||||
// Start both types of queries
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'auto'],
|
||||
queryFn: () => autoRefreshPromise,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: ['regular'],
|
||||
queryFn: () => regularPromise,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useIsGlobalTimeQueryRefreshing(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Should be true (auto-refresh is fetching)
|
||||
expect(result.current).toBe(true);
|
||||
|
||||
// Resolve auto-refresh query
|
||||
act(() => {
|
||||
resolveAutoRefresh({ data: 'done' });
|
||||
});
|
||||
|
||||
// Should be false even though regular query is still fetching
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
act(() => {
|
||||
resolveRegular({ data: 'done' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('scoped refreshing check with store name', () => {
|
||||
it('should return true only for queries matching store name', async () => {
|
||||
let resolveNamedQuery: (value: unknown) => void;
|
||||
const namedQueryPromise = new Promise((resolve) => {
|
||||
resolveNamedQuery = resolve;
|
||||
});
|
||||
|
||||
const wrapper = createProviderWrapper(
|
||||
{ name: 'drawer', initialTime: '15m' },
|
||||
queryClient,
|
||||
);
|
||||
|
||||
// Start query with matching name
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'drawer', 'test'],
|
||||
queryFn: () => namedQueryPromise,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Check refreshing status
|
||||
const { result } = renderHook(() => useIsGlobalTimeQueryRefreshing(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Should be true - named query is fetching
|
||||
expect(result.current).toBe(true);
|
||||
|
||||
// Resolve the query
|
||||
act(() => {
|
||||
resolveNamedQuery({ data: 'done' });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false when only different store queries are fetching', async () => {
|
||||
let resolveOtherQuery: (value: unknown) => void;
|
||||
const otherQueryPromise = new Promise((resolve) => {
|
||||
resolveOtherQuery = resolve;
|
||||
});
|
||||
|
||||
const wrapper = createProviderWrapper(
|
||||
{ name: 'drawer', initialTime: '15m' },
|
||||
queryClient,
|
||||
);
|
||||
|
||||
// Start query with different name (belongs to different store)
|
||||
renderHook(
|
||||
() =>
|
||||
useQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'other-store', 'test'],
|
||||
queryFn: () => otherQueryPromise,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Check refreshing status for 'drawer' store
|
||||
const { result } = renderHook(() => useIsGlobalTimeQueryRefreshing(), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Should be false - the fetching query belongs to 'other-store', not 'drawer'
|
||||
expect(result.current).toBe(false);
|
||||
|
||||
// Cleanup
|
||||
act(() => {
|
||||
resolveOtherQuery({ data: 'done' });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,190 @@
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
|
||||
import { createGlobalTimeStore, GlobalTimeStoreApi } from '../globalTimeStore';
|
||||
import { useQueryCacheSync } from '../useQueryCacheSync';
|
||||
|
||||
function createTestQueryClient(): QueryClient {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createWrapper(
|
||||
queryClient: QueryClient,
|
||||
): ({ children }: { children: ReactNode }) => JSX.Element {
|
||||
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
describe('useQueryCacheSync', () => {
|
||||
let store: GlobalTimeStoreApi;
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createGlobalTimeStore();
|
||||
queryClient = createTestQueryClient();
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:30:45.123Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
queryClient.clear();
|
||||
});
|
||||
|
||||
it('should update lastRefreshTimestamp when auto-refresh query succeeds', async () => {
|
||||
// Initialize store
|
||||
act(() => {
|
||||
store.getState().computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
const initialTimestamp = store.getState().lastRefreshTimestamp;
|
||||
|
||||
// Advance time
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
|
||||
// Render the hook
|
||||
renderHook(() => useQueryCacheSync(store), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
// Simulate a successful auto-refresh query
|
||||
await act(async () => {
|
||||
await queryClient.fetchQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'test'],
|
||||
queryFn: () => Promise.resolve({ data: 'test' }),
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(store.getState().lastRefreshTimestamp).toBeGreaterThan(
|
||||
initialTimestamp,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update timestamp for non-auto-refresh queries', async () => {
|
||||
act(() => {
|
||||
store.getState().computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
const initialTimestamp = store.getState().lastRefreshTimestamp;
|
||||
|
||||
renderHook(() => useQueryCacheSync(store), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
// Simulate a regular query (not auto-refresh)
|
||||
await act(async () => {
|
||||
await queryClient.fetchQuery({
|
||||
queryKey: ['some-other-query'],
|
||||
queryFn: () => Promise.resolve({ data: 'test' }),
|
||||
});
|
||||
});
|
||||
|
||||
expect(store.getState().lastRefreshTimestamp).toBe(initialTimestamp);
|
||||
});
|
||||
|
||||
describe('store name filtering', () => {
|
||||
it('should update timestamp for named store when matching query succeeds', async () => {
|
||||
const store = createGlobalTimeStore({ name: 'drawer' });
|
||||
|
||||
act(() => {
|
||||
store.getState().computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
const initialTimestamp = store.getState().lastRefreshTimestamp;
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
|
||||
renderHook(() => useQueryCacheSync(store), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
// Query with matching name
|
||||
await act(async () => {
|
||||
await queryClient.fetchQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'drawer', 'test'],
|
||||
queryFn: () => Promise.resolve({ data: 'test' }),
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(store.getState().lastRefreshTimestamp).toBeGreaterThan(
|
||||
initialTimestamp,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT update timestamp for named store when different name query succeeds', async () => {
|
||||
const store = createGlobalTimeStore({ name: 'drawer' });
|
||||
|
||||
act(() => {
|
||||
store.getState().computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
const initialTimestamp = store.getState().lastRefreshTimestamp;
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
|
||||
renderHook(() => useQueryCacheSync(store), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
// Query with different name
|
||||
await act(async () => {
|
||||
await queryClient.fetchQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'other-store', 'test'],
|
||||
queryFn: () => Promise.resolve({ data: 'test' }),
|
||||
});
|
||||
});
|
||||
|
||||
expect(store.getState().lastRefreshTimestamp).toBe(initialTimestamp);
|
||||
});
|
||||
|
||||
it('should NOT update timestamp for named store when unnamed query succeeds', async () => {
|
||||
const store = createGlobalTimeStore({ name: 'drawer' });
|
||||
|
||||
act(() => {
|
||||
store.getState().computeAndStoreMinMax();
|
||||
});
|
||||
|
||||
const initialTimestamp = store.getState().lastRefreshTimestamp;
|
||||
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(5000);
|
||||
});
|
||||
|
||||
renderHook(() => useQueryCacheSync(store), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
});
|
||||
|
||||
// Query without name (unnamed store format)
|
||||
await act(async () => {
|
||||
await queryClient.fetchQuery({
|
||||
queryKey: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, 'test-query'],
|
||||
queryFn: () => Promise.resolve({ data: 'test' }),
|
||||
});
|
||||
});
|
||||
|
||||
expect(store.getState().lastRefreshTimestamp).toBe(initialTimestamp);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,15 @@
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
|
||||
import {
|
||||
computeRounded5sMinMax,
|
||||
createCustomTimeRange,
|
||||
CUSTOM_TIME_SEPARATOR,
|
||||
getAutoRefreshQueryKey,
|
||||
isCustomTimeRange,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
parseCustomTimeRange,
|
||||
parseSelectedTime,
|
||||
roundDownTo5Seconds,
|
||||
} from '../utils';
|
||||
|
||||
describe('globalTime/utils', () => {
|
||||
@@ -59,7 +64,7 @@ describe('globalTime/utils', () => {
|
||||
const maxTime = 2000000000;
|
||||
const timeString = `${minTime}${CUSTOM_TIME_SEPARATOR}${maxTime}`;
|
||||
const result = parseCustomTimeRange(timeString);
|
||||
expect(result).toEqual({ minTime, maxTime });
|
||||
expect(result).toStrictEqual({ minTime, maxTime });
|
||||
});
|
||||
|
||||
it('should return null for non-custom time range strings', () => {
|
||||
@@ -75,7 +80,7 @@ describe('globalTime/utils', () => {
|
||||
|
||||
it('should handle zero values', () => {
|
||||
const result = parseCustomTimeRange(`0${CUSTOM_TIME_SEPARATOR}0`);
|
||||
expect(result).toEqual({ minTime: 0, maxTime: 0 });
|
||||
expect(result).toStrictEqual({ minTime: 0, maxTime: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,7 +99,7 @@ describe('globalTime/utils', () => {
|
||||
const maxTime = 2000000000;
|
||||
const timeString = createCustomTimeRange(minTime, maxTime);
|
||||
const result = parseSelectedTime(timeString);
|
||||
expect(result).toEqual({ minTime, maxTime });
|
||||
expect(result).toStrictEqual({ minTime, maxTime });
|
||||
});
|
||||
|
||||
it('should return fallback for invalid custom time range', () => {
|
||||
@@ -136,4 +141,184 @@ describe('globalTime/utils', () => {
|
||||
expect(result.minTime).toBe(now - oneDayNs);
|
||||
});
|
||||
});
|
||||
|
||||
describe('roundDownTo5Seconds', () => {
|
||||
it('should round down timestamp to 5-second boundary', () => {
|
||||
// 12:30:47.123Z -> 12:30:45.000Z
|
||||
const inputNano = 1705321847123 * NANO_SECOND_MULTIPLIER;
|
||||
const expectedNano = 1705321845000 * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
expect(roundDownTo5Seconds(inputNano)).toBe(expectedNano);
|
||||
});
|
||||
|
||||
it('should not change timestamp already at 5-second boundary', () => {
|
||||
const inputNano = 1705321845000 * NANO_SECOND_MULTIPLIER; // 12:30:45.000
|
||||
|
||||
expect(roundDownTo5Seconds(inputNano)).toBe(inputNano);
|
||||
});
|
||||
|
||||
it('should round 12:30:04.999 down to 12:30:00.000', () => {
|
||||
const inputNano = 1705321804999 * NANO_SECOND_MULTIPLIER;
|
||||
const expectedNano = 1705321800000 * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
expect(roundDownTo5Seconds(inputNano)).toBe(expectedNano);
|
||||
});
|
||||
|
||||
it('should round 12:30:09.999 down to 12:30:05.000', () => {
|
||||
const inputNano = 1705321809999 * NANO_SECOND_MULTIPLIER;
|
||||
const expectedNano = 1705321805000 * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
expect(roundDownTo5Seconds(inputNano)).toBe(expectedNano);
|
||||
});
|
||||
|
||||
it('should handle timestamp at exact 5-second intervals', () => {
|
||||
// Test 5, 10, 15, 20, 25... second marks
|
||||
const base = 1705321800000; // 12:30:00
|
||||
for (let sec = 0; sec < 60; sec += 5) {
|
||||
const inputNano = (base + sec * 1000) * NANO_SECOND_MULTIPLIER;
|
||||
expect(roundDownTo5Seconds(inputNano)).toBe(inputNano);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeRounded5sMinMax', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(new Date('2024-01-15T12:30:47.123Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should return maxTime rounded to 5-second boundary for relative time', () => {
|
||||
const result = computeRounded5sMinMax('15m');
|
||||
|
||||
// maxTime should be rounded down to 12:30:45.000
|
||||
const expectedMaxTime =
|
||||
new Date('2024-01-15T12:30:45.000Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
expect(result.maxTime).toBe(expectedMaxTime);
|
||||
});
|
||||
|
||||
it('should compute minTime based on 5s-rounded maxTime', () => {
|
||||
const result = computeRounded5sMinMax('15m');
|
||||
|
||||
const expectedMaxTime =
|
||||
new Date('2024-01-15T12:30:45.000Z').getTime() * NANO_SECOND_MULTIPLIER;
|
||||
const fifteenMinutesNs = 15 * 60 * 1000 * NANO_SECOND_MULTIPLIER;
|
||||
|
||||
expect(result.minTime).toBe(expectedMaxTime - fifteenMinutesNs);
|
||||
});
|
||||
|
||||
it('should return unchanged values for custom time range', () => {
|
||||
const minTime = 1000000000;
|
||||
const maxTime = 2000000000;
|
||||
const customTime = createCustomTimeRange(minTime, maxTime);
|
||||
|
||||
const result = computeRounded5sMinMax(customTime);
|
||||
|
||||
expect(result.minTime).toBe(minTime);
|
||||
expect(result.maxTime).toBe(maxTime);
|
||||
});
|
||||
|
||||
it('should preserve duration for 1h relative time', () => {
|
||||
const result = computeRounded5sMinMax('1h');
|
||||
|
||||
const oneHourNs = 60 * 60 * 1000 * NANO_SECOND_MULTIPLIER;
|
||||
const duration = result.maxTime - result.minTime;
|
||||
|
||||
expect(duration).toBe(oneHourNs);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAutoRefreshQueryKey', () => {
|
||||
it('should prefix with AUTO_REFRESH_QUERY constant', () => {
|
||||
const result = getAutoRefreshQueryKey('15m', 'MY_QUERY');
|
||||
|
||||
expect(result[0]).toBe(REACT_QUERY_KEY.AUTO_REFRESH_QUERY);
|
||||
});
|
||||
|
||||
it('should append selectedTime at end', () => {
|
||||
const result = getAutoRefreshQueryKey('15m', 'MY_QUERY', 'param1');
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
REACT_QUERY_KEY.AUTO_REFRESH_QUERY,
|
||||
'MY_QUERY',
|
||||
'param1',
|
||||
'15m',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle no additional query parts', () => {
|
||||
const result = getAutoRefreshQueryKey('1h');
|
||||
|
||||
expect(result).toStrictEqual([REACT_QUERY_KEY.AUTO_REFRESH_QUERY, '1h']);
|
||||
});
|
||||
|
||||
it('should handle custom time range as selectedTime', () => {
|
||||
const customTime = createCustomTimeRange(1000000000, 2000000000);
|
||||
const result = getAutoRefreshQueryKey(customTime, 'METRICS');
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
REACT_QUERY_KEY.AUTO_REFRESH_QUERY,
|
||||
'METRICS',
|
||||
customTime,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle object query parts', () => {
|
||||
const params = { entityId: '123', filter: 'active' };
|
||||
const result = getAutoRefreshQueryKey('15m', 'ENTITY', params);
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
REACT_QUERY_KEY.AUTO_REFRESH_QUERY,
|
||||
'ENTITY',
|
||||
params,
|
||||
'15m',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAutoRefreshQueryKey deprecation', () => {
|
||||
const originalEnv = process.env.NODE_ENV;
|
||||
const originalWarn = console.warn;
|
||||
|
||||
beforeEach(() => {
|
||||
console.warn = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
console.warn = originalWarn;
|
||||
});
|
||||
|
||||
it('should log deprecation warning in development', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
getAutoRefreshQueryKey('15m', 'TEST');
|
||||
|
||||
expect(console.warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('deprecated'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT log deprecation warning in production', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
getAutoRefreshQueryKey('15m', 'TEST');
|
||||
|
||||
expect(console.warn).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should still return correct query key format', () => {
|
||||
const result = getAutoRefreshQueryKey('15m', 'MY_QUERY', 'param1');
|
||||
|
||||
expect(result).toStrictEqual([
|
||||
REACT_QUERY_KEY.AUTO_REFRESH_QUERY,
|
||||
'MY_QUERY',
|
||||
'param1',
|
||||
'15m',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,32 +1,144 @@
|
||||
import { createStore, StoreApi, useStore } from 'zustand';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import {
|
||||
IGlobalTimeStoreActions,
|
||||
IGlobalTimeStoreState,
|
||||
GlobalTimeSelectedTime,
|
||||
GlobalTimeState,
|
||||
GlobalTimeStore,
|
||||
ParsedTimeRange,
|
||||
} from './types';
|
||||
import { isCustomTimeRange, parseSelectedTime } from './utils';
|
||||
import {
|
||||
computeRounded5sMinMax,
|
||||
isCustomTimeRange,
|
||||
parseSelectedTime,
|
||||
} from './utils';
|
||||
|
||||
export type IGlobalTimeStore = IGlobalTimeStoreState & IGlobalTimeStoreActions;
|
||||
export type GlobalTimeStoreApi = StoreApi<GlobalTimeStore>;
|
||||
export type IGlobalTimeStore = GlobalTimeStore;
|
||||
|
||||
export const useGlobalTimeStore = create<IGlobalTimeStore>((set, get) => ({
|
||||
selectedTime: DEFAULT_TIME_RANGE,
|
||||
isRefreshEnabled: false,
|
||||
refreshInterval: 0,
|
||||
setSelectedTime: (selectedTime, refreshInterval): void => {
|
||||
set((state) => {
|
||||
const newRefreshInterval = refreshInterval ?? state.refreshInterval;
|
||||
const isCustom = isCustomTimeRange(selectedTime);
|
||||
function computeIsRefreshEnabled(
|
||||
selectedTime: GlobalTimeSelectedTime,
|
||||
refreshInterval: number,
|
||||
): boolean {
|
||||
if (isCustomTimeRange(selectedTime)) {
|
||||
return false;
|
||||
}
|
||||
return refreshInterval > 0;
|
||||
}
|
||||
|
||||
return {
|
||||
selectedTime,
|
||||
refreshInterval: newRefreshInterval,
|
||||
isRefreshEnabled: !isCustom && newRefreshInterval > 0,
|
||||
};
|
||||
});
|
||||
},
|
||||
getMinMaxTime: (selectedTime): ParsedTimeRange => {
|
||||
return parseSelectedTime(selectedTime || get().selectedTime);
|
||||
},
|
||||
}));
|
||||
export function createGlobalTimeStore(
|
||||
initialState?: Partial<GlobalTimeState>,
|
||||
): GlobalTimeStoreApi {
|
||||
const selectedTime = initialState?.selectedTime ?? DEFAULT_TIME_RANGE;
|
||||
const refreshInterval = initialState?.refreshInterval ?? 0;
|
||||
const name = initialState?.name;
|
||||
|
||||
return createStore<GlobalTimeStore>((set, get) => ({
|
||||
name,
|
||||
selectedTime,
|
||||
refreshInterval,
|
||||
isRefreshEnabled: computeIsRefreshEnabled(selectedTime, refreshInterval),
|
||||
lastRefreshTimestamp: 0,
|
||||
lastComputedMinMax: { minTime: 0, maxTime: 0 },
|
||||
|
||||
setSelectedTime: (
|
||||
time: GlobalTimeSelectedTime,
|
||||
newRefreshInterval?: number,
|
||||
): void => {
|
||||
const state = get();
|
||||
const interval = newRefreshInterval ?? state.refreshInterval;
|
||||
|
||||
if (time === state.selectedTime && interval === state.refreshInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
const computedMinMax = parseSelectedTime(time);
|
||||
|
||||
set({
|
||||
selectedTime: time,
|
||||
refreshInterval: interval,
|
||||
isRefreshEnabled: computeIsRefreshEnabled(time, interval),
|
||||
lastComputedMinMax: computedMinMax,
|
||||
lastRefreshTimestamp: Date.now(),
|
||||
});
|
||||
},
|
||||
|
||||
setRefreshInterval: (interval: number): void => {
|
||||
set((state) => ({
|
||||
refreshInterval: interval,
|
||||
isRefreshEnabled: computeIsRefreshEnabled(state.selectedTime, interval),
|
||||
}));
|
||||
},
|
||||
|
||||
getMinMaxTime: (): ParsedTimeRange => {
|
||||
const state = get();
|
||||
|
||||
if (isCustomTimeRange(state.selectedTime)) {
|
||||
return parseSelectedTime(state.selectedTime);
|
||||
}
|
||||
|
||||
if (state.isRefreshEnabled) {
|
||||
const freshMinMax = computeRounded5sMinMax(state.selectedTime);
|
||||
|
||||
if (
|
||||
freshMinMax.minTime !== state.lastComputedMinMax.minTime ||
|
||||
freshMinMax.maxTime !== state.lastComputedMinMax.maxTime
|
||||
) {
|
||||
set({ lastComputedMinMax: freshMinMax, lastRefreshTimestamp: Date.now() });
|
||||
}
|
||||
|
||||
return freshMinMax;
|
||||
}
|
||||
|
||||
return state.lastComputedMinMax;
|
||||
},
|
||||
|
||||
computeAndStoreMinMax: (): ParsedTimeRange => {
|
||||
const state = get();
|
||||
|
||||
if (state.isRefreshEnabled) {
|
||||
return state.lastComputedMinMax;
|
||||
}
|
||||
|
||||
const computedMinMax = parseSelectedTime(state.selectedTime);
|
||||
|
||||
set({
|
||||
lastComputedMinMax: computedMinMax,
|
||||
lastRefreshTimestamp: Date.now(),
|
||||
});
|
||||
return computedMinMax;
|
||||
},
|
||||
|
||||
updateRefreshTimestamp: (): void => {
|
||||
set({ lastRefreshTimestamp: Date.now() });
|
||||
},
|
||||
|
||||
getAutoRefreshQueryKey: (
|
||||
selectedTime: GlobalTimeSelectedTime,
|
||||
...queryParts: unknown[]
|
||||
): unknown[] => {
|
||||
const storeName = get().name;
|
||||
if (storeName) {
|
||||
return [
|
||||
REACT_QUERY_KEY.AUTO_REFRESH_QUERY,
|
||||
storeName,
|
||||
...queryParts,
|
||||
selectedTime,
|
||||
];
|
||||
}
|
||||
return [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, ...queryParts, selectedTime];
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
export const defaultGlobalTimeStore = createGlobalTimeStore();
|
||||
|
||||
export const useGlobalTimeStore = <T = GlobalTimeStore>(
|
||||
selector?: (state: GlobalTimeStore) => T,
|
||||
): T => {
|
||||
return useStore(
|
||||
defaultGlobalTimeStore,
|
||||
selector ?? ((state) => state as unknown as T),
|
||||
);
|
||||
};
|
||||
|
||||
57
frontend/src/store/globalTime/hooks.ts
Normal file
57
frontend/src/store/globalTime/hooks.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
// oxlint-disable-next-line no-restricted-imports
|
||||
import { useContext } from 'react';
|
||||
import { useStoreWithEqualityFn } from 'zustand/traditional';
|
||||
|
||||
import { GlobalTimeContext } from './GlobalTimeContext';
|
||||
import { defaultGlobalTimeStore, GlobalTimeStoreApi } from './globalTimeStore';
|
||||
import { GlobalTimeStore, ParsedTimeRange } from './types';
|
||||
import { isCustomTimeRange } from './utils';
|
||||
|
||||
/**
|
||||
* Access global time state with optional selector for performance.
|
||||
*
|
||||
* @example
|
||||
* // Full state (re-renders on any change)
|
||||
* const { selectedTime, setSelectedTime } = useGlobalTime();
|
||||
*
|
||||
* @example
|
||||
* // With selector (re-renders only when selectedTime changes)
|
||||
* const selectedTime = useGlobalTime(state => state.selectedTime);
|
||||
*/
|
||||
export function useGlobalTime<T = GlobalTimeStore>(
|
||||
selector?: (state: GlobalTimeStore) => T,
|
||||
equalityFn?: (a: T, b: T) => boolean,
|
||||
): T {
|
||||
const contextStore = useContext(GlobalTimeContext);
|
||||
const store = contextStore ?? defaultGlobalTimeStore;
|
||||
|
||||
return useStoreWithEqualityFn(
|
||||
store,
|
||||
selector ?? ((state) => state as unknown as T),
|
||||
equalityFn,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if currently using a custom time range.
|
||||
*/
|
||||
export function useIsCustomTimeRange(): boolean {
|
||||
const selectedTime = useGlobalTime((state) => state.selectedTime);
|
||||
return isCustomTimeRange(selectedTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the store API directly (for subscriptions or non-React contexts).
|
||||
*/
|
||||
export function useGlobalTimeStoreApi(): GlobalTimeStoreApi {
|
||||
const contextStore = useContext(GlobalTimeContext);
|
||||
return contextStore ?? defaultGlobalTimeStore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last computed min/max time values.
|
||||
* Use this for display purposes to ensure consistency with query data.
|
||||
*/
|
||||
export function useLastComputedMinMax(): ParsedTimeRange {
|
||||
return useGlobalTime((state) => state.lastComputedMinMax);
|
||||
}
|
||||
@@ -1,9 +1,558 @@
|
||||
export { useGlobalTimeStore } from './globalTimeStore';
|
||||
export type { IGlobalTimeStoreState, ParsedTimeRange } from './types';
|
||||
/**
|
||||
* # Global Time Store
|
||||
*
|
||||
* Centralized time management for the application with auto-refresh support.
|
||||
*
|
||||
* ## Quick Start
|
||||
*
|
||||
* ```tsx
|
||||
* import { useGlobalTime, NANO_SECOND_MULTIPLIER } from 'store/globalTime';
|
||||
*
|
||||
* function MyComponent() {
|
||||
* const selectedTime = useGlobalTime((s) => s.selectedTime);
|
||||
* const getMinMaxTime = useGlobalTime((s) => s.getMinMaxTime);
|
||||
* const getAutoRefreshQueryKey = useGlobalTime((s) => s.getAutoRefreshQueryKey);
|
||||
* const isRefreshEnabled = useGlobalTime((s) => s.isRefreshEnabled);
|
||||
* const refreshInterval = useGlobalTime((s) => s.refreshInterval);
|
||||
*
|
||||
* const { data } = useQuery({
|
||||
* queryKey: getAutoRefreshQueryKey(selectedTime, 'MY_QUERY', params),
|
||||
* queryFn: () => {
|
||||
* const { minTime, maxTime } = getMinMaxTime();
|
||||
* const start = Math.floor(minTime / NANO_SECOND_MULTIPLIER / 1000);
|
||||
* const end = Math.floor(maxTime / NANO_SECOND_MULTIPLIER / 1000);
|
||||
* return fetchData({ start, end });
|
||||
* },
|
||||
* refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
* });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ## Core Concepts
|
||||
*
|
||||
* ### Time Formats
|
||||
*
|
||||
* | Format | Example | Description |
|
||||
* |--------|---------|-------------|
|
||||
* | Relative | `'15m'`, `'1h'`, `'1d'` | Duration from now, supports auto-refresh |
|
||||
* | Custom | `'1234567890||_||1234567899'` | Fixed range in nanoseconds, no auto-refresh |
|
||||
*
|
||||
* ### Time Units
|
||||
*
|
||||
* - Store values are in **nanoseconds**
|
||||
* - Most APIs expect **seconds**
|
||||
* - Convert to have seconds: `Math.floor(nanoTime / NANO_SECOND_MULTIPLIER / 1000)`
|
||||
* - Convert to have ms: `Math.floor(nanoTime / NANO_SECOND_MULTIPLIER)`
|
||||
*
|
||||
* ## Integration Guide
|
||||
*
|
||||
* ### Step 1: Get Store State
|
||||
*
|
||||
* Use selectors for optimal re-render performance:
|
||||
*
|
||||
* ```tsx
|
||||
* // Good - only re-renders when selectedTime changes
|
||||
* const selectedTime = useGlobalTime((s) => s.selectedTime);
|
||||
* const getMinMaxTime = useGlobalTime((s) => s.getMinMaxTime);
|
||||
*
|
||||
* // Avoid - re-renders on ANY store change
|
||||
* const store = useGlobalTime();
|
||||
* ```
|
||||
*
|
||||
* ### Step 2: Build Query Key
|
||||
*
|
||||
* Use the store's `getAutoRefreshQueryKey` to enable auto-refresh:
|
||||
*
|
||||
* ```tsx
|
||||
* const getAutoRefreshQueryKey = useGlobalTime((s) => s.getAutoRefreshQueryKey);
|
||||
*
|
||||
* const queryKey = useMemo(
|
||||
* () => getAutoRefreshQueryKey(
|
||||
* selectedTime, // Required - triggers invalidation
|
||||
* 'UNIQUE_KEY', // Your query identifier
|
||||
* ...otherParams // Additional cache-busting params
|
||||
* ),
|
||||
* [getAutoRefreshQueryKey, selectedTime, ...deps]
|
||||
* );
|
||||
* ```
|
||||
*
|
||||
* **Note:** For named providers (with `name` prop), query keys are automatically
|
||||
* scoped to that store, enabling isolated invalidation and refresh tracking.
|
||||
*
|
||||
* ### Step 3: Fetch Data
|
||||
*
|
||||
* **IMPORTANT**: Call `getMinMaxTime()` INSIDE `queryFn`:
|
||||
*
|
||||
* ```tsx
|
||||
* const { data } = useQuery({
|
||||
* queryKey,
|
||||
* queryFn: () => {
|
||||
* // Fresh time values computed here during auto-refresh
|
||||
* const { minTime, maxTime } = getMinMaxTime();
|
||||
* const start = Math.floor(minTime / NANO_SECOND_MULTIPLIER / 1000);
|
||||
* const end = Math.floor(maxTime / NANO_SECOND_MULTIPLIER / 1000);
|
||||
* return api.fetch({ start, end });
|
||||
* },
|
||||
* refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ### Step 4: Add Refresh Button (Optional)
|
||||
*
|
||||
* ```tsx
|
||||
* import {
|
||||
* useGlobalTimeQueryInvalidate,
|
||||
* useIsGlobalTimeQueryRefreshing,
|
||||
* } from 'store/globalTime';
|
||||
*
|
||||
* function RefreshButton() {
|
||||
* const invalidate = useGlobalTimeQueryInvalidate();
|
||||
* const isRefreshing = useIsGlobalTimeQueryRefreshing();
|
||||
*
|
||||
* return (
|
||||
* <button onClick={invalidate} disabled={isRefreshing}>
|
||||
* {isRefreshing ? 'Refreshing...' : 'Refresh'}
|
||||
* </button>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ## Avoiding Stale Data
|
||||
*
|
||||
* ### Problem: Time Drift During Refresh
|
||||
*
|
||||
* If multiple queries compute time independently, they may use different values:
|
||||
*
|
||||
* ```tsx
|
||||
* // BAD - each query gets different time
|
||||
* queryFn: () => {
|
||||
* const now = Date.now();
|
||||
* return fetchData({ end: now, start: now - duration });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Solution: Use getMinMaxTime()
|
||||
*
|
||||
* `getMinMaxTime()` ensures all queries use consistent timestamps:
|
||||
* - When auto-refresh is **disabled**: returns cached values from `computeAndStoreMinMax()`
|
||||
* - When auto-refresh is **enabled**: computes fresh values (rounded to 5-second boundaries)
|
||||
*
|
||||
* Since values are rounded to 5-second boundaries, all queries calling `getMinMaxTime()`
|
||||
* within the same 5-second window get identical timestamps.
|
||||
*
|
||||
* ```tsx
|
||||
* // GOOD - all queries get same time
|
||||
* queryFn: () => {
|
||||
* const { minTime, maxTime } = getMinMaxTime();
|
||||
* return fetchData({ start: minTime, end: maxTime });
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### How It Works
|
||||
*
|
||||
* **Manual refresh:**
|
||||
* 1. User clicks refresh
|
||||
* 2. `useGlobalTimeQueryInvalidate` calls `computeAndStoreMinMax()`
|
||||
* 3. Fresh min/max stored in `lastComputedMinMax`
|
||||
* 4. All queries re-run and call `getMinMaxTime()`
|
||||
* 5. All get the SAME cached values
|
||||
*
|
||||
* **Auto-refresh (when `isRefreshEnabled = true`):**
|
||||
* 1. React-query's `refetchInterval` triggers query re-execution
|
||||
* 2. `getMinMaxTime()` computes fresh values (rounded to 5 seconds)
|
||||
* 3. If values changed, updates `lastComputedMinMax` cache
|
||||
* 4. All queries within same 5-second window get consistent values
|
||||
*
|
||||
* ## Auto-Refresh Setup
|
||||
*
|
||||
* Auto-refresh is enabled when:
|
||||
* - `selectedTime` is a relative duration (e.g., `'15m'`)
|
||||
* - `refreshInterval > 0`
|
||||
*
|
||||
* ```tsx
|
||||
* // Auto-refresh configuration
|
||||
* const selectedTime = useGlobalTime((s) => s.selectedTime);
|
||||
* const getAutoRefreshQueryKey = useGlobalTime((s) => s.getAutoRefreshQueryKey);
|
||||
* const isRefreshEnabled = useGlobalTime((s) => s.isRefreshEnabled);
|
||||
* const refreshInterval = useGlobalTime((s) => s.refreshInterval);
|
||||
*
|
||||
* useQuery({
|
||||
* queryKey: getAutoRefreshQueryKey(selectedTime, 'MY_QUERY'),
|
||||
* queryFn: () => { ... },
|
||||
* // Enable periodic refetch
|
||||
* refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* ## API Reference
|
||||
*
|
||||
* ### Hooks
|
||||
*
|
||||
* | Hook | Returns | Description |
|
||||
* |------|---------|-------------|
|
||||
* | `useGlobalTime(selector?)` | `T` | Access store state with optional selector |
|
||||
* | `useGlobalTimeQueryInvalidate()` | `() => Promise<void>` | Invalidate all auto-refresh queries |
|
||||
* | `useIsGlobalTimeQueryRefreshing()` | `boolean` | Check if any query is refreshing |
|
||||
* | `useIsCustomTimeRange()` | `boolean` | Check if using fixed time range |
|
||||
* | `useLastComputedMinMax()` | `ParsedTimeRange` | Get cached min/max values |
|
||||
* | `useGlobalTimeStoreApi()` | `GlobalTimeStoreApi` | Get raw store API |
|
||||
*
|
||||
* ### Store Actions
|
||||
*
|
||||
* | Action | Description |
|
||||
* |--------|-------------|
|
||||
* | `setSelectedTime(time, interval?)` | Set time range and optional refresh interval (resets cache) |
|
||||
* | `setRefreshInterval(ms)` | Set auto-refresh interval |
|
||||
* | `getMinMaxTime(time?)` | Get min/max (fresh if auto-refresh enabled, cached otherwise) |
|
||||
* | `computeAndStoreMinMax()` | Compute fresh values and cache them |
|
||||
* | `getAutoRefreshQueryKey(time, ...parts)` | Build scoped query key for this store instance |
|
||||
*
|
||||
* ### Utilities
|
||||
*
|
||||
* | Function | Description |
|
||||
* |----------|-------------|
|
||||
* | `getAutoRefreshQueryKey(time, ...parts)` | **@deprecated** Use store action instead |
|
||||
* | `parseSelectedTime(time)` | Parse time string to min/max (fresh computation) |
|
||||
* | `isCustomTimeRange(time)` | Check if time is custom range format |
|
||||
* | `createCustomTimeRange(min, max)` | Create custom range string |
|
||||
*
|
||||
* ### Constants
|
||||
*
|
||||
* | Constant | Value | Description |
|
||||
* |----------|-------|-------------|
|
||||
* | `NANO_SECOND_MULTIPLIER` | `1000000` | Convert ms to ns |
|
||||
* | `CUSTOM_TIME_SEPARATOR` | `'||_||'` | Separator in custom range strings |
|
||||
*
|
||||
* ## Context & Composition
|
||||
*
|
||||
* ### Why Use Context?
|
||||
*
|
||||
* By default, `useGlobalTime()` uses a shared global store. Use `GlobalTimeProvider`
|
||||
* to create isolated time state for specific UI sections (modals, drawers, etc.).
|
||||
*
|
||||
* ### Provider Options
|
||||
*
|
||||
* | Option | Type | Description |
|
||||
* |--------|------|-------------|
|
||||
* | `name` | `string` | Scope query keys to this store (enables isolated invalidation) |
|
||||
* | `inheritGlobalTime` | `boolean` | Initialize with parent/global time value |
|
||||
* | `initialTime` | `string` | Initial time if not inheriting |
|
||||
* | `enableUrlParams` | `boolean \| object` | Sync time to URL query params |
|
||||
* | `removeQueryParamsOnUnmount` | `boolean` | Clean URL params on unmount |
|
||||
* | `localStoragePersistKey` | `string` | Persist time to localStorage |
|
||||
* | `refreshInterval` | `number` | Initial auto-refresh interval (ms) |
|
||||
*
|
||||
* ### Example 1: Isolated Time in Modal
|
||||
*
|
||||
* A modal with its own time picker that doesn't affect the main page:
|
||||
*
|
||||
* ```tsx
|
||||
* import { GlobalTimeProvider, useGlobalTime } from 'store/globalTime';
|
||||
*
|
||||
* function EntityDetailsModal({ entity, onClose }) {
|
||||
* return (
|
||||
* <Modal open onClose={onClose}>
|
||||
* // Isolated time context - changes here don't affect parent
|
||||
* <GlobalTimeProvider
|
||||
* inheritGlobalTime // Start with parent's current time
|
||||
* refreshInterval={0} // No auto-refresh in modal
|
||||
* >
|
||||
* <ModalContent entity={entity} />
|
||||
* </GlobalTimeProvider>
|
||||
* </Modal>
|
||||
* );
|
||||
* }
|
||||
*
|
||||
* function ModalContent({ entity }) {
|
||||
* // This useGlobalTime reads from the modal's isolated store
|
||||
* const selectedTime = useGlobalTime((s) => s.selectedTime);
|
||||
* const setSelectedTime = useGlobalTime((s) => s.setSelectedTime);
|
||||
*
|
||||
* return (
|
||||
* <>
|
||||
* <DateTimePicker
|
||||
* value={selectedTime}
|
||||
* onChange={(time) => setSelectedTime(time)}
|
||||
* />
|
||||
* <EntityMetrics entity={entity} />
|
||||
* <EntityLogs entity={entity} />
|
||||
* </>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Example 2: List Page with Detail Drawer
|
||||
*
|
||||
* Main list uses global time, drawer has independent time:
|
||||
*
|
||||
* ```tsx
|
||||
* // Main list page - uses global time (no provider needed)
|
||||
* function K8sPodsList() {
|
||||
* const selectedTime = useGlobalTime((s) => s.selectedTime);
|
||||
* const [selectedPod, setSelectedPod] = useState(null);
|
||||
*
|
||||
* return (
|
||||
* <>
|
||||
* <PageHeader>
|
||||
* <DateTimeSelectionV3 /> // Controls global time
|
||||
* </PageHeader>
|
||||
*
|
||||
* <PodsTable
|
||||
* timeRange={selectedTime}
|
||||
* onRowClick={setSelectedPod}
|
||||
* />
|
||||
*
|
||||
* {selectedPod && (
|
||||
* <PodDetailsDrawer
|
||||
* pod={selectedPod}
|
||||
* onClose={() => setSelectedPod(null)}
|
||||
* />
|
||||
* )}
|
||||
* </>
|
||||
* );
|
||||
* }
|
||||
*
|
||||
* // Drawer with its own time context
|
||||
* function PodDetailsDrawer({ pod, onClose }) {
|
||||
* return (
|
||||
* <Drawer open onClose={onClose}>
|
||||
* <GlobalTimeProvider
|
||||
* name="pod-drawer" // Scopes queries - only this drawer's queries are invalidated
|
||||
* inheritGlobalTime // Start with list's time
|
||||
* removeQueryParamsOnUnmount // Clean up URL when drawer closes
|
||||
* enableUrlParams={{
|
||||
* relativeTimeKey: 'drawerTime',
|
||||
* startTimeKey: 'drawerStart',
|
||||
* endTimeKey: 'drawerEnd',
|
||||
* }}
|
||||
* >
|
||||
* <DrawerHeader>
|
||||
* <DateTimeSelectionV3 /> // Controls drawer's time only
|
||||
* </DrawerHeader>
|
||||
*
|
||||
* <Tabs>
|
||||
* <Tab label="Metrics"><PodMetrics pod={pod} /></Tab>
|
||||
* <Tab label="Logs"><PodLogs pod={pod} /></Tab>
|
||||
* <Tab label="Events"><PodEvents pod={pod} /></Tab>
|
||||
* </Tabs>
|
||||
* </GlobalTimeProvider>
|
||||
* </Drawer>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Example 3: Nested Contexts
|
||||
*
|
||||
* Contexts can be nested - each level creates isolation:
|
||||
*
|
||||
* ```tsx
|
||||
* // App level - global time
|
||||
* function App() {
|
||||
* return (
|
||||
* <QueryClientProvider>
|
||||
* // No provider here = uses defaultGlobalTimeStore
|
||||
* <Dashboard />
|
||||
* </QueryClientProvider>
|
||||
* );
|
||||
* }
|
||||
*
|
||||
* // Dashboard with comparison panel
|
||||
* function Dashboard() {
|
||||
* return (
|
||||
* <div className="dashboard">
|
||||
* // Main dashboard uses global time
|
||||
* <MainCharts />
|
||||
*
|
||||
* // Comparison panel has its own time
|
||||
* <GlobalTimeProvider initialTime="1h">
|
||||
* <ComparisonPanel />
|
||||
* </GlobalTimeProvider>
|
||||
* </div>
|
||||
* );
|
||||
* }
|
||||
*
|
||||
* function ComparisonPanel() {
|
||||
* // This reads from ComparisonPanel's isolated store (1h)
|
||||
* // Not affected by global time changes
|
||||
* const selectedTime = useGlobalTime((s) => s.selectedTime);
|
||||
* return <ComparisonCharts timeRange={selectedTime} />;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Example 4: URL Sync for Shareable Links
|
||||
*
|
||||
* Persist time selection to URL for shareable links:
|
||||
*
|
||||
* ```tsx
|
||||
* function TracesExplorer() {
|
||||
* return (
|
||||
* <GlobalTimeProvider
|
||||
* enableUrlParams={{
|
||||
* relativeTimeKey: 'time', // ?time=15m
|
||||
* startTimeKey: 'startTime', // ?startTime=1234567890
|
||||
* endTimeKey: 'endTime', // ?endTime=1234567899
|
||||
* }}
|
||||
* initialTime="15m" // Fallback if URL has no time params
|
||||
* >
|
||||
* <TracesContent />
|
||||
* </GlobalTimeProvider>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Example 5: localStorage Persistence
|
||||
*
|
||||
* Remember user's last selected time across sessions:
|
||||
*
|
||||
* ```tsx
|
||||
* function MetricsExplorer() {
|
||||
* return (
|
||||
* <GlobalTimeProvider
|
||||
* localStoragePersistKey="metrics-explorer-time"
|
||||
* initialTime="1h" // Fallback for first visit
|
||||
* >
|
||||
* <MetricsContent />
|
||||
* </GlobalTimeProvider>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ### Context Resolution Order
|
||||
*
|
||||
* When `useGlobalTime()` is called, it resolves the store in this order:
|
||||
*
|
||||
* 1. Nearest `GlobalTimeProvider` ancestor (if any)
|
||||
* 2. `defaultGlobalTimeStore` (global singleton)
|
||||
*
|
||||
* ```
|
||||
* App (no provider -> uses defaultGlobalTimeStore)
|
||||
* |-- Dashboard
|
||||
* |-- MainCharts (uses defaultGlobalTimeStore)
|
||||
* |-- GlobalTimeProvider (isolated store A)
|
||||
* |-- ComparisonPanel (uses store A)
|
||||
* |-- GlobalTimeProvider (isolated store B)
|
||||
* |-- NestedChart (uses store B)
|
||||
* ```
|
||||
*
|
||||
* ### Scoped Query Keys with `name`
|
||||
*
|
||||
* The `name` prop enables isolated query invalidation. When a provider has a name,
|
||||
* its queries are prefixed with that name, so invalidation only affects that store:
|
||||
*
|
||||
* ```tsx
|
||||
* // Main page - unnamed store
|
||||
* // Query keys: ['AUTO_REFRESH_QUERY', 'METRICS', ...]
|
||||
* function MainDashboard() {
|
||||
* const getAutoRefreshQueryKey = useGlobalTime((s) => s.getAutoRefreshQueryKey);
|
||||
* // ...
|
||||
* }
|
||||
*
|
||||
* // Drawer - named store
|
||||
* // Query keys: ['AUTO_REFRESH_QUERY', 'drawer', 'METRICS', ...]
|
||||
* function DetailDrawer() {
|
||||
* return (
|
||||
* <GlobalTimeProvider name="drawer" inheritGlobalTime>
|
||||
* <DrawerContent />
|
||||
* </GlobalTimeProvider>
|
||||
* );
|
||||
* }
|
||||
*
|
||||
* function DrawerContent() {
|
||||
* const getAutoRefreshQueryKey = useGlobalTime((s) => s.getAutoRefreshQueryKey);
|
||||
* const invalidate = useGlobalTimeQueryInvalidate();
|
||||
* // invalidate() only refreshes queries with 'drawer' prefix
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* ## Complete Example
|
||||
*
|
||||
* ```tsx
|
||||
* import { useMemo } from 'react';
|
||||
* import { useQuery } from 'react-query';
|
||||
* import { useGlobalTime, NANO_SECOND_MULTIPLIER } from 'store/globalTime';
|
||||
*
|
||||
* function MetricsPanel({ entityId }: { entityId: string }) {
|
||||
* // 1. Get store state with selectors
|
||||
* const selectedTime = useGlobalTime((s) => s.selectedTime);
|
||||
* const getMinMaxTime = useGlobalTime((s) => s.getMinMaxTime);
|
||||
* const getAutoRefreshQueryKey = useGlobalTime((s) => s.getAutoRefreshQueryKey);
|
||||
* const isRefreshEnabled = useGlobalTime((s) => s.isRefreshEnabled);
|
||||
* const refreshInterval = useGlobalTime((s) => s.refreshInterval);
|
||||
*
|
||||
* // 2. Build query key (memoized) - automatically scoped if using named provider
|
||||
* const queryKey = useMemo(
|
||||
* () => getAutoRefreshQueryKey(selectedTime, 'METRICS', entityId),
|
||||
* [getAutoRefreshQueryKey, selectedTime, entityId]
|
||||
* );
|
||||
*
|
||||
* // 3. Query with auto-refresh
|
||||
* const { data, isLoading } = useQuery({
|
||||
* queryKey,
|
||||
* queryFn: () => {
|
||||
* // Get fresh time inside queryFn
|
||||
* const { minTime, maxTime } = getMinMaxTime();
|
||||
* const start = Math.floor(minTime / NANO_SECOND_MULTIPLIER / 1000);
|
||||
* const end = Math.floor(maxTime / NANO_SECOND_MULTIPLIER / 1000);
|
||||
*
|
||||
* return fetchMetrics({ entityId, start, end });
|
||||
* },
|
||||
* refetchInterval: isRefreshEnabled ? refreshInterval : false,
|
||||
* });
|
||||
*
|
||||
* return <Chart data={data} loading={isLoading} />;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @module store/globalTime
|
||||
*/
|
||||
|
||||
// Store
|
||||
export {
|
||||
createGlobalTimeStore,
|
||||
defaultGlobalTimeStore,
|
||||
useGlobalTimeStore,
|
||||
} from './globalTimeStore';
|
||||
export type { GlobalTimeStoreApi } from './globalTimeStore';
|
||||
|
||||
// Context & Provider
|
||||
export { GlobalTimeContext, GlobalTimeProvider } from './GlobalTimeContext';
|
||||
|
||||
// Hooks
|
||||
export {
|
||||
useGlobalTime,
|
||||
useGlobalTimeStoreApi,
|
||||
useIsCustomTimeRange,
|
||||
useLastComputedMinMax,
|
||||
} from './hooks';
|
||||
|
||||
// Query hooks for auto-refresh
|
||||
export { useGlobalTimeQueryInvalidate } from './useGlobalTimeQueryInvalidate';
|
||||
export { useIsGlobalTimeQueryRefreshing } from './useIsGlobalTimeQueryRefreshing';
|
||||
|
||||
// Types
|
||||
export type {
|
||||
CustomTimeRange,
|
||||
CustomTimeRangeSeparator,
|
||||
GlobalTimeActions,
|
||||
GlobalTimeProviderOptions,
|
||||
GlobalTimeSelectedTime,
|
||||
GlobalTimeState,
|
||||
GlobalTimeStore,
|
||||
IGlobalTimeStoreActions,
|
||||
IGlobalTimeStoreState,
|
||||
ParsedTimeRange,
|
||||
} from './types';
|
||||
|
||||
// Utilities
|
||||
export {
|
||||
createCustomTimeRange,
|
||||
CUSTOM_TIME_SEPARATOR,
|
||||
getAutoRefreshQueryKey,
|
||||
isCustomTimeRange,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
parseCustomTimeRange,
|
||||
parseSelectedTime,
|
||||
} from './utils';
|
||||
|
||||
// Internal hooks (for advanced use cases)
|
||||
export { useQueryCacheSync } from './useQueryCacheSync';
|
||||
|
||||
@@ -44,9 +44,80 @@ export interface IGlobalTimeStoreActions {
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Get the current min/max time values parsed from selectedTime.
|
||||
* For durations, computes fresh values based on Date.now().
|
||||
* For custom ranges, extracts the stored values.
|
||||
* Get the current min/max time values.
|
||||
* - Custom time ranges: returns exact parsed values
|
||||
* - isRefreshEnabled true: computes 5s-rounded values and updates store
|
||||
* - isRefreshEnabled false: returns lastComputedMinMax
|
||||
*/
|
||||
getMinMaxTime: (selectedItem?: GlobalTimeSelectedTime) => ParsedTimeRange;
|
||||
getMinMaxTime: () => ParsedTimeRange;
|
||||
}
|
||||
|
||||
export interface GlobalTimeProviderOptions {
|
||||
/**
|
||||
* Optional name for the store instance.
|
||||
* Used to scope query keys - only queries with this store's prefix
|
||||
* will be tracked/invalidated by this store's hooks.
|
||||
*/
|
||||
name?: string;
|
||||
/** Initialize from parent/global time */
|
||||
inheritGlobalTime?: boolean;
|
||||
/** Initial time if not inheriting */
|
||||
initialTime?: GlobalTimeSelectedTime;
|
||||
/** URL sync configuration. When false/omitted, no URL sync. */
|
||||
enableUrlParams?:
|
||||
| boolean
|
||||
| {
|
||||
relativeTimeKey?: string;
|
||||
startTimeKey?: string;
|
||||
endTimeKey?: string;
|
||||
};
|
||||
removeQueryParamsOnUnmount?: boolean;
|
||||
localStoragePersistKey?: string;
|
||||
refreshInterval?: number;
|
||||
}
|
||||
|
||||
export interface GlobalTimeState {
|
||||
/**
|
||||
* Optional name for the store instance.
|
||||
* Used to scope query keys for auto-refresh queries.
|
||||
* Unnamed stores use the default prefix without a name.
|
||||
*/
|
||||
name?: string;
|
||||
selectedTime: GlobalTimeSelectedTime;
|
||||
refreshInterval: number;
|
||||
isRefreshEnabled: boolean;
|
||||
lastRefreshTimestamp: number;
|
||||
lastComputedMinMax: ParsedTimeRange;
|
||||
}
|
||||
|
||||
export interface GlobalTimeActions {
|
||||
setSelectedTime: (
|
||||
time: GlobalTimeSelectedTime,
|
||||
refreshInterval?: number,
|
||||
) => void;
|
||||
setRefreshInterval: (interval: number) => void;
|
||||
getMinMaxTime: () => ParsedTimeRange;
|
||||
/**
|
||||
* Compute fresh rounded min/max values, store them, and update refresh timestamp.
|
||||
* Call this before invalidating queries to ensure all queries use the same time values.
|
||||
*
|
||||
* @returns The newly computed ParsedTimeRange
|
||||
*/
|
||||
computeAndStoreMinMax: () => ParsedTimeRange;
|
||||
/**
|
||||
* Update the refresh timestamp to current time.
|
||||
* Called by QueryCache listener when auto-refresh queries complete.
|
||||
*/
|
||||
updateRefreshTimestamp: () => void;
|
||||
/**
|
||||
* Build query key for auto-refresh queries scoped to this store.
|
||||
* Named stores: ['AUTO_REFRESH_QUERY', name, ...parts, selectedTime]
|
||||
* Unnamed stores: ['AUTO_REFRESH_QUERY', ...parts, selectedTime]
|
||||
*/
|
||||
getAutoRefreshQueryKey: (
|
||||
selectedTime: GlobalTimeSelectedTime,
|
||||
...queryParts: unknown[]
|
||||
) => unknown[];
|
||||
}
|
||||
|
||||
export type GlobalTimeStore = GlobalTimeState & GlobalTimeActions;
|
||||
|
||||
18
frontend/src/store/globalTime/useComputedMinMaxSync.ts
Normal file
18
frontend/src/store/globalTime/useComputedMinMaxSync.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { GlobalTimeStoreApi } from './globalTimeStore';
|
||||
|
||||
/**
|
||||
* Used to initialize computed min/max on mount when store has no values yet.
|
||||
* setSelectedTime now computes min/max on change, so subscription is no longer needed.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function useComputedMinMaxSync(store: GlobalTimeStoreApi): void {
|
||||
useEffect(() => {
|
||||
const { lastComputedMinMax } = store.getState();
|
||||
if (lastComputedMinMax.minTime === 0 && lastComputedMinMax.maxTime === 0) {
|
||||
store.getState().computeAndStoreMinMax();
|
||||
}
|
||||
}, [store]);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGlobalTime } from './hooks';
|
||||
|
||||
/**
|
||||
* Use when you want to invalidate any query tracked by {@link REACT_QUERY_KEY.AUTO_REFRESH_QUERY}
|
||||
*
|
||||
* This hook computes fresh time values before invalidating queries,
|
||||
* ensuring all queries use the same min/max time during a refresh cycle.
|
||||
*
|
||||
* For named stores, only invalidates queries matching the store's name.
|
||||
* For unnamed stores, invalidates all AUTO_REFRESH_QUERY queries.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function useGlobalTimeQueryInvalidate(): () => Promise<void> {
|
||||
const queryClient = useQueryClient();
|
||||
const computeAndStoreMinMax = useGlobalTime((s) => s.computeAndStoreMinMax);
|
||||
const name = useGlobalTime((s) => s.name);
|
||||
|
||||
return useCallback(async () => {
|
||||
// Compute fresh time values BEFORE invalidating
|
||||
// This ensures all queries that re-run will use the same time values
|
||||
// If refresh is enabled, this will just be skipped
|
||||
computeAndStoreMinMax();
|
||||
|
||||
// Build scoped query key prefix
|
||||
const queryKey = name
|
||||
? [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, name]
|
||||
: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY];
|
||||
|
||||
return await queryClient.invalidateQueries({ queryKey });
|
||||
}, [queryClient, computeAndStoreMinMax, name]);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useIsFetching } from 'react-query';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useGlobalTime } from './hooks';
|
||||
|
||||
/**
|
||||
* Use when you want to know if any query tracked by {@link REACT_QUERY_KEY.AUTO_REFRESH_QUERY} is refreshing
|
||||
*
|
||||
* For named stores, only checks queries matching the store's name.
|
||||
* For unnamed stores, checks all AUTO_REFRESH_QUERY queries.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function useIsGlobalTimeQueryRefreshing(): boolean {
|
||||
const name = useGlobalTime((s) => s.name);
|
||||
|
||||
const queryKey = name
|
||||
? [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, name]
|
||||
: [REACT_QUERY_KEY.AUTO_REFRESH_QUERY];
|
||||
|
||||
return useIsFetching({ queryKey }) > 0;
|
||||
}
|
||||
32
frontend/src/store/globalTime/usePersistence.ts
Normal file
32
frontend/src/store/globalTime/usePersistence.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import set from 'api/browser/localstorage/set';
|
||||
|
||||
import { GlobalTimeStoreApi } from './globalTimeStore';
|
||||
|
||||
/**
|
||||
* Used to keep the selected time persisted on localStorage
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function usePersistence(
|
||||
store: GlobalTimeStoreApi,
|
||||
persistKey: string | undefined,
|
||||
): void {
|
||||
useEffect(() => {
|
||||
if (!persistKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
let previousSelectedTime = store.getState().selectedTime;
|
||||
|
||||
return store.subscribe((state) => {
|
||||
if (state.selectedTime === previousSelectedTime) {
|
||||
return;
|
||||
}
|
||||
previousSelectedTime = state.selectedTime;
|
||||
|
||||
set(persistKey, state.selectedTime);
|
||||
});
|
||||
}, [store, persistKey]);
|
||||
}
|
||||
51
frontend/src/store/globalTime/useQueryCacheSync.ts
Normal file
51
frontend/src/store/globalTime/useQueryCacheSync.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
|
||||
import { GlobalTimeStoreApi } from './globalTimeStore';
|
||||
|
||||
/**
|
||||
* Used to keep lastRefreshTimestamp in sync after every react query refresh.
|
||||
* For named stores, only tracks queries with matching store name.
|
||||
* For unnamed stores, tracks all AUTO_REFRESH_QUERY queries (backward compatible).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function useQueryCacheSync(store: GlobalTimeStoreApi): void {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
const queryCache = queryClient.getQueryCache();
|
||||
const storeName = store.getState().name;
|
||||
|
||||
return queryCache.subscribe((event) => {
|
||||
if (event?.type !== 'queryUpdated') {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = event.action as { type?: string };
|
||||
if (action?.type !== 'success') {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryKey = event.query.queryKey;
|
||||
if (!Array.isArray(queryKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this is created by getAutoRefreshQueryKey inside the store,
|
||||
// to track usages of global time store and autoRefresh
|
||||
if (queryKey[0] !== REACT_QUERY_KEY.AUTO_REFRESH_QUERY) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Named store: only track queries with matching name at position [1]
|
||||
if (storeName && queryKey[1] !== storeName) {
|
||||
return;
|
||||
}
|
||||
// Unnamed store: track all AUTO_REFRESH_QUERY queries (backward compatible)
|
||||
store.getState().updateRefreshTimestamp();
|
||||
});
|
||||
}, [queryClient, store]);
|
||||
}
|
||||
145
frontend/src/store/globalTime/useUrlSync.ts
Normal file
145
frontend/src/store/globalTime/useUrlSync.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { isValidShortHandDateTimeFormat } from 'lib/getMinMax';
|
||||
|
||||
import { GlobalTimeStoreApi } from './globalTimeStore';
|
||||
import { GlobalTimeProviderOptions } from './types';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
isCustomTimeRange,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
parseCustomTimeRange,
|
||||
} from './utils';
|
||||
|
||||
interface UrlSyncConfig {
|
||||
relativeTimeKey: string;
|
||||
startTimeKey: string;
|
||||
endTimeKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to sync internal state with URL when URL params are enabled.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function useUrlSync(
|
||||
store: GlobalTimeStoreApi,
|
||||
enableUrlParams: GlobalTimeProviderOptions['enableUrlParams'],
|
||||
removeOnUnmount: boolean,
|
||||
): void {
|
||||
const isInitialMount = useRef(true);
|
||||
|
||||
const keys: UrlSyncConfig =
|
||||
enableUrlParams && typeof enableUrlParams === 'object'
|
||||
? {
|
||||
relativeTimeKey: enableUrlParams.relativeTimeKey ?? 'relativeTime',
|
||||
startTimeKey: enableUrlParams.startTimeKey ?? 'startTime',
|
||||
endTimeKey: enableUrlParams.endTimeKey ?? 'endTime',
|
||||
}
|
||||
: {
|
||||
relativeTimeKey: 'relativeTime',
|
||||
startTimeKey: 'startTime',
|
||||
endTimeKey: 'endTime',
|
||||
};
|
||||
|
||||
const [urlState, setUrlState] = useQueryStates(
|
||||
{
|
||||
[keys.relativeTimeKey]: parseAsString,
|
||||
[keys.startTimeKey]: parseAsInteger,
|
||||
[keys.endTimeKey]: parseAsInteger,
|
||||
},
|
||||
{ history: 'replace' },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableUrlParams || !isInitialMount.current) {
|
||||
return;
|
||||
}
|
||||
isInitialMount.current = false;
|
||||
|
||||
const relativeTime = urlState[keys.relativeTimeKey];
|
||||
const startTime = urlState[keys.startTimeKey];
|
||||
const endTime = urlState[keys.endTimeKey];
|
||||
|
||||
if (typeof startTime === 'number' && typeof endTime === 'number') {
|
||||
const customTime = createCustomTimeRange(
|
||||
startTime * NANO_SECOND_MULTIPLIER,
|
||||
endTime * NANO_SECOND_MULTIPLIER,
|
||||
);
|
||||
store.getState().setSelectedTime(customTime);
|
||||
} else if (
|
||||
typeof relativeTime === 'string' &&
|
||||
isValidShortHandDateTimeFormat(relativeTime)
|
||||
) {
|
||||
store.getState().setSelectedTime(relativeTime as Time);
|
||||
}
|
||||
}, [
|
||||
urlState,
|
||||
keys?.startTimeKey,
|
||||
keys?.endTimeKey,
|
||||
keys?.relativeTimeKey,
|
||||
store,
|
||||
enableUrlParams,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableUrlParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
let previousSelectedTime = store.getState().selectedTime;
|
||||
|
||||
return store.subscribe((state) => {
|
||||
if (state.selectedTime === previousSelectedTime) {
|
||||
return;
|
||||
}
|
||||
previousSelectedTime = state.selectedTime;
|
||||
|
||||
if (isCustomTimeRange(state.selectedTime)) {
|
||||
const parsed = parseCustomTimeRange(state.selectedTime);
|
||||
if (parsed) {
|
||||
void setUrlState({
|
||||
[keys.relativeTimeKey]: null,
|
||||
[keys.startTimeKey]: Math.floor(parsed.minTime / NANO_SECOND_MULTIPLIER),
|
||||
[keys.endTimeKey]: Math.floor(parsed.maxTime / NANO_SECOND_MULTIPLIER),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
void setUrlState({
|
||||
[keys.relativeTimeKey]: state.selectedTime,
|
||||
[keys.startTimeKey]: null,
|
||||
[keys.endTimeKey]: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [
|
||||
store,
|
||||
keys?.startTimeKey,
|
||||
keys?.endTimeKey,
|
||||
keys?.relativeTimeKey,
|
||||
setUrlState,
|
||||
enableUrlParams,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableUrlParams || !removeOnUnmount) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (): void => {
|
||||
void setUrlState({
|
||||
[keys.relativeTimeKey]: null,
|
||||
[keys.startTimeKey]: null,
|
||||
[keys.endTimeKey]: null,
|
||||
});
|
||||
};
|
||||
}, [
|
||||
removeOnUnmount,
|
||||
keys?.relativeTimeKey,
|
||||
keys?.startTimeKey,
|
||||
keys?.endTimeKey,
|
||||
setUrlState,
|
||||
enableUrlParams,
|
||||
]);
|
||||
}
|
||||
@@ -44,8 +44,8 @@ export function parseCustomTimeRange(
|
||||
}
|
||||
|
||||
const [minStr, maxStr] = selectedTime.split(CUSTOM_TIME_SEPARATOR);
|
||||
const minTime = parseInt(minStr, 10);
|
||||
const maxTime = parseInt(maxStr, 10);
|
||||
const minTime = Number.parseInt(minStr, 10);
|
||||
const maxTime = Number.parseInt(maxStr, 10);
|
||||
|
||||
if (Number.isNaN(minTime) || Number.isNaN(maxTime)) {
|
||||
return null;
|
||||
@@ -79,11 +79,60 @@ export function parseSelectedTime(selectedTime: string): ParsedTimeRange {
|
||||
}
|
||||
|
||||
/**
|
||||
* Use to build your react-query key for auto-refresh queries
|
||||
* @deprecated Use store.getAutoRefreshQueryKey() instead.
|
||||
* Access via: const getAutoRefreshQueryKey = useGlobalTime((s) => s.getAutoRefreshQueryKey);
|
||||
*
|
||||
* This function only works with the default (unnamed) store prefix.
|
||||
* For named stores, use the store method to get properly scoped query keys.
|
||||
*/
|
||||
export function getAutoRefreshQueryKey(
|
||||
selectedTime: GlobalTimeSelectedTime,
|
||||
...queryParts: unknown[]
|
||||
): unknown[] {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(
|
||||
'[globalTime] getAutoRefreshQueryKey from utils is deprecated. ' +
|
||||
'Use useGlobalTime((s) => s.getAutoRefreshQueryKey) instead.',
|
||||
);
|
||||
}
|
||||
return [REACT_QUERY_KEY.AUTO_REFRESH_QUERY, ...queryParts, selectedTime];
|
||||
}
|
||||
|
||||
/**
|
||||
* Round timestamp down to the nearest 5-second boundary.
|
||||
* Used for tighter sync during auto-refresh scenarios.
|
||||
*
|
||||
* @param timestampNano - Timestamp in nanoseconds
|
||||
* @returns Timestamp rounded down to 5-second boundary in nanoseconds
|
||||
*/
|
||||
export function roundDownTo5Seconds(timestampNano: number): number {
|
||||
const msPerInterval = 5 * 1000;
|
||||
const timestampMs = Math.floor(timestampNano / NANO_SECOND_MULTIPLIER);
|
||||
const roundedMs = Math.floor(timestampMs / msPerInterval) * msPerInterval;
|
||||
return roundedMs * NANO_SECOND_MULTIPLIER;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute min/max time with maxTime rounded down to 5-second boundary.
|
||||
* Used when isRefreshEnabled is true for tighter time sync.
|
||||
*
|
||||
* @param selectedTime - The selected time (relative like '15m' or custom range)
|
||||
* @returns ParsedTimeRange with 5-second rounded maxTime for relative times
|
||||
*/
|
||||
export function computeRounded5sMinMax(selectedTime: string): ParsedTimeRange {
|
||||
if (isCustomTimeRange(selectedTime)) {
|
||||
return parseSelectedTime(selectedTime);
|
||||
}
|
||||
|
||||
const nowNano = Date.now() * NANO_SECOND_MULTIPLIER;
|
||||
const roundedMaxTime = roundDownTo5Seconds(nowNano);
|
||||
|
||||
const { minTime: originalMin, maxTime: originalMax } =
|
||||
getMinMaxForSelectedTime(selectedTime as Time, 0, 0);
|
||||
const durationNano = originalMax - originalMin;
|
||||
|
||||
return {
|
||||
minTime: roundedMaxTime - durationNano,
|
||||
maxTime: roundedMaxTime,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ export function getAppContextMock(
|
||||
status: '',
|
||||
updated_at: '0',
|
||||
},
|
||||
state: LicenseState.ACTIVATED,
|
||||
state: LicenseState.ACTIVE,
|
||||
status: LicenseStatus.VALID,
|
||||
platform: LicensePlatform.CLOUD,
|
||||
created_at: '0',
|
||||
|
||||
@@ -11,7 +11,7 @@ export enum LicenseStatus {
|
||||
|
||||
export enum LicenseState {
|
||||
DEFAULTED = 'DEFAULTED',
|
||||
ACTIVATED = 'ACTIVATED',
|
||||
ACTIVE = 'ACTIVE',
|
||||
EXPIRED = 'EXPIRED',
|
||||
ISSUED = 'ISSUED',
|
||||
EVALUATING = 'EVALUATING',
|
||||
|
||||
@@ -19141,6 +19141,11 @@ use-sidecar@^1.1.3:
|
||||
detect-node-es "^1.1.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
use-sync-external-store@1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d"
|
||||
integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==
|
||||
|
||||
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
|
||||
|
||||
@@ -16,8 +16,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
"github.com/SigNoz/signoz/pkg/modules/promote"
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
@@ -63,7 +62,6 @@ type provider struct {
|
||||
factoryHandler factory.Handler
|
||||
cloudIntegrationHandler cloudintegration.Handler
|
||||
ruleStateHistoryHandler rulestatehistory.Handler
|
||||
spanMapperHandler spanmapper.Handler
|
||||
alertmanagerHandler alertmanager.Handler
|
||||
traceDetailHandler tracedetail.Handler
|
||||
rulerHandler ruler.Handler
|
||||
@@ -94,7 +92,6 @@ func NewFactory(
|
||||
factoryHandler factory.Handler,
|
||||
cloudIntegrationHandler cloudintegration.Handler,
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
spanMapperHandler spanmapper.Handler,
|
||||
alertmanagerHandler alertmanager.Handler,
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
@@ -128,7 +125,6 @@ func NewFactory(
|
||||
factoryHandler,
|
||||
cloudIntegrationHandler,
|
||||
ruleStateHistoryHandler,
|
||||
spanMapperHandler,
|
||||
alertmanagerHandler,
|
||||
traceDetailHandler,
|
||||
rulerHandler,
|
||||
@@ -164,7 +160,6 @@ func newProvider(
|
||||
factoryHandler factory.Handler,
|
||||
cloudIntegrationHandler cloudintegration.Handler,
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
spanMapperHandler spanmapper.Handler,
|
||||
alertmanagerHandler alertmanager.Handler,
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
@@ -198,7 +193,6 @@ func newProvider(
|
||||
factoryHandler: factoryHandler,
|
||||
cloudIntegrationHandler: cloudIntegrationHandler,
|
||||
ruleStateHistoryHandler: ruleStateHistoryHandler,
|
||||
spanMapperHandler: spanMapperHandler,
|
||||
alertmanagerHandler: alertmanagerHandler,
|
||||
traceDetailHandler: traceDetailHandler,
|
||||
rulerHandler: rulerHandler,
|
||||
@@ -306,10 +300,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addSpanMapperRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addAlertmanagerRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/span_mapper_groups", handler.New(
|
||||
provider.authZ.ViewAccess(provider.spanMapperHandler.ListGroups),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListSpanMapperGroups",
|
||||
Tags: []string{"spanmapper"},
|
||||
Summary: "List span attribute mapping groups",
|
||||
Description: "Returns all span attribute mapping groups for the authenticated org.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
RequestQuery: new(spantypes.ListSpanMapperGroupsQuery),
|
||||
Response: new(spantypes.GettableSpanMapperGroups),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/span_mapper_groups", handler.New(
|
||||
provider.authZ.AdminAccess(provider.spanMapperHandler.CreateGroup),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateSpanMapperGroup",
|
||||
Tags: []string{"spanmapper"},
|
||||
Summary: "Create a span attribute mapping group",
|
||||
Description: "Creates a new span attribute mapping group for the org.",
|
||||
Request: new(spantypes.PostableSpanMapperGroup),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(spantypes.GettableSpanMapperGroup),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}", handler.New(
|
||||
provider.authZ.AdminAccess(provider.spanMapperHandler.UpdateGroup),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateSpanMapperGroup",
|
||||
Tags: []string{"spanmapper"},
|
||||
Summary: "Update a span attribute mapping group",
|
||||
Description: "Partially updates an existing mapping group's name, condition, or enabled state.",
|
||||
Request: new(spantypes.UpdatableSpanMapperGroup),
|
||||
RequestContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}", handler.New(
|
||||
provider.authZ.AdminAccess(provider.spanMapperHandler.DeleteGroup),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteSpanMapperGroup",
|
||||
Tags: []string{"spanmapper"},
|
||||
Summary: "Delete a span attribute mapping group",
|
||||
Description: "Hard-deletes a mapping group and cascades to all its mappers.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}/span_mappers", handler.New(
|
||||
provider.authZ.ViewAccess(provider.spanMapperHandler.ListMappers),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListSpanMappers",
|
||||
Tags: []string{"spanmapper"},
|
||||
Summary: "List span mappers for a group",
|
||||
Description: "Returns all mappers belonging to a mapping group.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(spantypes.GettableSpanMapperGroups),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}/span_mappers", handler.New(
|
||||
provider.authZ.AdminAccess(provider.spanMapperHandler.CreateMapper),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateSpanMapper",
|
||||
Tags: []string{"spanmapper"},
|
||||
Summary: "Create a span mapper",
|
||||
Description: "Adds a new mapper to the specified mapping group.",
|
||||
Request: new(spantypes.PostableSpanMapper),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(spantypes.GettableSpanMapper),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusCreated,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound, http.StatusConflict},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}/span_mappers/{mapperId}", handler.New(
|
||||
provider.authZ.AdminAccess(provider.spanMapperHandler.UpdateMapper),
|
||||
handler.OpenAPIDef{
|
||||
ID: "UpdateSpanMapper",
|
||||
Tags: []string{"spanmapper"},
|
||||
Summary: "Update a span mapper",
|
||||
Description: "Partially updates an existing mapper's field context, config, or enabled state.",
|
||||
Request: new(spantypes.UpdatableSpanMapper),
|
||||
RequestContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodPatch).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}/span_mappers/{mapperId}", handler.New(
|
||||
provider.authZ.AdminAccess(provider.spanMapperHandler.DeleteMapper),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteSpanMapper",
|
||||
Tags: []string{"spanmapper"},
|
||||
Summary: "Delete a span mapper",
|
||||
Description: "Hard-deletes a mapper from a mapping group.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
package implspanmapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type handler struct {
|
||||
module spanmapper.Module
|
||||
providerSettings factory.ProviderSettings
|
||||
}
|
||||
|
||||
func NewHandler(module spanmapper.Module, providerSettings factory.ProviderSettings) spanmapper.Handler {
|
||||
return &handler{module: module, providerSettings: providerSettings}
|
||||
}
|
||||
|
||||
// ListGroups handles GET /api/v1/span_mapper_groups.
|
||||
func (h *handler) ListGroups(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
var q spantypes.ListSpanMapperGroupsQuery
|
||||
if err := binding.Query.BindQuery(r.URL.Query(), &q); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
groups, err := h.module.ListGroups(ctx, orgID, &q)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, spantypes.NewGettableSpanMapperGroups(groups))
|
||||
}
|
||||
|
||||
// CreateGroup handles POST /api/v1/span_mapper_groups.
|
||||
func (h *handler) CreateGroup(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
req := new(spantypes.PostableSpanMapperGroup)
|
||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
group := spantypes.NewSpanMapperGroupFromPostable(req)
|
||||
|
||||
err = h.module.CreateGroup(ctx, orgID, claims.Email, group)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
render.Success(rw, http.StatusCreated, group)
|
||||
}
|
||||
|
||||
// UpdateGroup handles PUT /api/v1/span_mapper_groups/{id}.
|
||||
func (h *handler) UpdateGroup(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
id, err := groupIDFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := new(spantypes.UpdatableSpanMapperGroup)
|
||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.module.UpdateGroup(ctx, orgID, id, claims.Email, spantypes.NewSpanMapperGroupFromUpdatable(req))
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// DeleteGroup handles DELETE /api/v1/span_mapper_groups/{id}.
|
||||
func (h *handler) DeleteGroup(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
id, err := groupIDFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.module.DeleteGroup(ctx, orgID, id); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// ListMappers handles GET /api/v1/span_mapper_groups/{id}/span_mappers.
|
||||
func (h *handler) ListMappers(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
groupID, err := groupIDFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
mappers, err := h.module.ListMappers(ctx, orgID, groupID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, spantypes.NewGettableSpanMappers(mappers))
|
||||
}
|
||||
|
||||
// CreateMapper handles POST /api/v1/span_mapper_groups/{id}/span_mappers.
|
||||
func (h *handler) CreateMapper(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
groupID, err := groupIDFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := new(spantypes.PostableSpanMapper)
|
||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
mapper := spantypes.NewSpanMapperFromPostable(req)
|
||||
|
||||
err = h.module.CreateMapper(ctx, orgID, groupID, claims.Email, mapper)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusCreated, mapper)
|
||||
}
|
||||
|
||||
// UpdateMapper handles PUT /api/v1/span_mapper_groups/{groupId}/span_mappers/{mapperId}.
|
||||
func (h *handler) UpdateMapper(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
groupID, err := groupIDFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
mapperID, err := mapperIDFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
req := new(spantypes.UpdatableSpanMapper)
|
||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.module.UpdateMapper(ctx, orgID, groupID, mapperID, claims.Email, spantypes.NewSpanMapperFromUpdatable(req))
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// DeleteMapper handles DELETE /api/v1/span_mapper_groups/{groupId}/span_mappers/{mapperId}.
|
||||
func (h *handler) DeleteMapper(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
groupID, err := groupIDFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
mapperID, err := mapperIDFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.module.DeleteMapper(ctx, orgID, groupID, mapperID); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// groupIDFromPath extracts and validates the {id} or {groupId} path variable.
|
||||
func groupIDFromPath(r *http.Request) (valuer.UUID, error) {
|
||||
vars := mux.Vars(r)
|
||||
raw := vars["groupId"]
|
||||
id, err := valuer.NewUUID(raw)
|
||||
if err != nil {
|
||||
return valuer.UUID{}, errors.Wrapf(err, errors.TypeInvalidInput, spantypes.ErrCodeMappingInvalidInput, "group id is not a valid uuid")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// mapperIDFromPath extracts and validates the {mapperId} path variable.
|
||||
func mapperIDFromPath(r *http.Request) (valuer.UUID, error) {
|
||||
raw := mux.Vars(r)["mapperId"]
|
||||
id, err := valuer.NewUUID(raw)
|
||||
if err != nil {
|
||||
return valuer.UUID{}, errors.Wrapf(err, errors.TypeInvalidInput, spantypes.ErrCodeMappingInvalidInput, "mapper id is not a valid uuid")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package spanmapper
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/spantypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// Module defines the business logic for span attribute mapping groups and mappers.
|
||||
type Module interface {
|
||||
// Group operations
|
||||
ListGroups(ctx context.Context, orgID valuer.UUID, q *spantypes.ListSpanMapperGroupsQuery) ([]*spantypes.SpanMapperGroup, error)
|
||||
GetGroup(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*spantypes.SpanMapperGroup, error)
|
||||
CreateGroup(ctx context.Context, orgID valuer.UUID, createdBy string, group *spantypes.SpanMapperGroup) error
|
||||
UpdateGroup(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, group *spantypes.SpanMapperGroup) error
|
||||
DeleteGroup(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
|
||||
|
||||
// Mapper operations
|
||||
ListMappers(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID) ([]*spantypes.SpanMapper, error)
|
||||
GetMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, id valuer.UUID) (*spantypes.SpanMapper, error)
|
||||
CreateMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, createdBy string, mapper *spantypes.SpanMapper) error
|
||||
UpdateMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, id valuer.UUID, updatedBy string, mapper *spantypes.SpanMapper) error
|
||||
DeleteMapper(ctx context.Context, orgID valuer.UUID, groupID valuer.UUID, id valuer.UUID) error
|
||||
}
|
||||
|
||||
// Handler defines the HTTP handler interface for mapping group and mapper endpoints.
|
||||
type Handler interface {
|
||||
// Group handlers
|
||||
ListGroups(rw http.ResponseWriter, r *http.Request)
|
||||
CreateGroup(rw http.ResponseWriter, r *http.Request)
|
||||
UpdateGroup(rw http.ResponseWriter, r *http.Request)
|
||||
DeleteGroup(rw http.ResponseWriter, r *http.Request)
|
||||
|
||||
// Mapper handlers
|
||||
ListMappers(rw http.ResponseWriter, r *http.Request)
|
||||
CreateMapper(rw http.ResponseWriter, r *http.Request)
|
||||
UpdateMapper(rw http.ResponseWriter, r *http.Request)
|
||||
DeleteMapper(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
@@ -36,8 +36,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount/implserviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/services"
|
||||
"github.com/SigNoz/signoz/pkg/modules/services/implservices"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper/implspanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanpercentile"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanpercentile/implspanpercentile"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
@@ -73,7 +71,6 @@ type Handlers struct {
|
||||
RegistryHandler factory.Handler
|
||||
CloudIntegrationHandler cloudintegration.Handler
|
||||
RuleStateHistory rulestatehistory.Handler
|
||||
SpanMapperHandler spanmapper.Handler
|
||||
AlertmanagerHandler alertmanager.Handler
|
||||
TraceDetail tracedetail.Handler
|
||||
RulerHandler ruler.Handler
|
||||
@@ -117,7 +114,6 @@ func NewHandlers(
|
||||
RegistryHandler: registryHandler,
|
||||
RuleStateHistory: implrulestatehistory.NewHandler(modules.RuleStateHistory),
|
||||
CloudIntegrationHandler: implcloudintegration.NewHandler(modules.CloudIntegration),
|
||||
SpanMapperHandler: implspanmapper.NewHandler(nil, providerSettings), // todo(nitya): will update this in future PR
|
||||
AlertmanagerHandler: signozalertmanager.NewHandler(alertmanagerService),
|
||||
TraceDetail: impltracedetail.NewHandler(modules.TraceDetail),
|
||||
RulerHandler: signozruler.NewHandler(rulerService),
|
||||
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
"github.com/SigNoz/signoz/pkg/modules/promote"
|
||||
@@ -30,7 +30,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/rulestatehistory"
|
||||
"github.com/SigNoz/signoz/pkg/modules/serviceaccount"
|
||||
"github.com/SigNoz/signoz/pkg/modules/session"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/modules/tracedetail"
|
||||
"github.com/SigNoz/signoz/pkg/modules/user"
|
||||
"github.com/SigNoz/signoz/pkg/querier"
|
||||
@@ -75,7 +74,6 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ factory.Handler }{},
|
||||
struct{ cloudintegration.Handler }{},
|
||||
struct{ rulestatehistory.Handler }{},
|
||||
struct{ spanmapper.Handler }{},
|
||||
struct{ alertmanager.Handler }{},
|
||||
struct{ tracedetail.Handler }{},
|
||||
struct{ ruler.Handler }{},
|
||||
|
||||
@@ -281,7 +281,6 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.RegistryHandler,
|
||||
handlers.CloudIntegrationHandler,
|
||||
handlers.RuleStateHistory,
|
||||
handlers.SpanMapperHandler,
|
||||
handlers.AlertmanagerHandler,
|
||||
handlers.TraceDetail,
|
||||
handlers.RulerHandler,
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
package spantypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeMapperNotFound = errors.MustNewCode("span_attribute_mapper_not_found")
|
||||
ErrCodeMapperAlreadyExists = errors.MustNewCode("span_attribute_mapper_already_exists")
|
||||
ErrCodeMappingInvalidInput = errors.MustNewCode("span_attribute_mapping_invalid_input")
|
||||
)
|
||||
|
||||
// FieldContext is where the target attribute is written.
|
||||
type FieldContext struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
FieldContextSpanAttribute = FieldContext{valuer.NewString("attribute")}
|
||||
FieldContextResource = FieldContext{valuer.NewString("resource")}
|
||||
)
|
||||
|
||||
// MapperOperation determines whether the source attribute is moved (deleted) or copied.
|
||||
type SpanMapperOperation struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
SpanMapperOperationMove = SpanMapperOperation{valuer.NewString("move")}
|
||||
SpanMapperOperationCopy = SpanMapperOperation{valuer.NewString("copy")}
|
||||
)
|
||||
|
||||
// MapperSource describes one candidate source for a target attribute.
|
||||
type SpanMapperSource struct {
|
||||
Key string `json:"key" required:"true"`
|
||||
Context FieldContext `json:"context" required:"true"`
|
||||
Operation SpanMapperOperation `json:"operation" required:"true"`
|
||||
Priority int `json:"priority" required:"true"`
|
||||
}
|
||||
|
||||
// MapperConfig holds the mapping logic for a single target attribute.
|
||||
// It implements driver.Valuer and sql.Scanner for JSON text column storage.
|
||||
type SpanMapperConfig struct {
|
||||
Sources []SpanMapperSource `json:"sources" required:"true" nullable:"true"`
|
||||
}
|
||||
|
||||
// SpanMapper is the domain model for a span attribute mapper.
|
||||
type SpanMapper struct {
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
|
||||
ID valuer.UUID `json:"id" required:"true"`
|
||||
GroupID valuer.UUID `json:"group_id" required:"true"`
|
||||
Name string `json:"name" required:"true"`
|
||||
FieldContext FieldContext `json:"field_context" required:"true"`
|
||||
Config SpanMapperConfig `json:"config" required:"true"`
|
||||
Enabled bool `json:"enabled" required:"true"`
|
||||
}
|
||||
|
||||
type PostableSpanMapper struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
FieldContext FieldContext `json:"field_context" required:"true"`
|
||||
Config SpanMapperConfig `json:"config" required:"true"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// UpdatableSpanMapper is the HTTP request body for updating a span mapper.
|
||||
// All fields are optional; only non-nil fields are applied.
|
||||
type UpdatableSpanMapper struct {
|
||||
FieldContext FieldContext `json:"field_context,omitempty"`
|
||||
Config *SpanMapperConfig `json:"config,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
}
|
||||
|
||||
type GettableSpanMapper = SpanMapper
|
||||
|
||||
type GettableSpanMappers struct {
|
||||
Items []*GettableSpanMapper `json:"items" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
func (FieldContext) Enum() []any {
|
||||
return []any{FieldContextSpanAttribute, FieldContextResource}
|
||||
}
|
||||
|
||||
func (SpanMapperOperation) Enum() []any {
|
||||
return []any{SpanMapperOperationMove, SpanMapperOperationCopy}
|
||||
}
|
||||
|
||||
func NewSpanMapperFromStorable(s *StorableSpanMapper) *SpanMapper {
|
||||
return &SpanMapper{
|
||||
TimeAuditable: s.TimeAuditable,
|
||||
UserAuditable: s.UserAuditable,
|
||||
ID: s.ID,
|
||||
GroupID: s.GroupID,
|
||||
Name: s.Name,
|
||||
FieldContext: s.FieldContext,
|
||||
Config: s.Config,
|
||||
Enabled: s.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpanMapperFromPostable(req *PostableSpanMapper) *SpanMapper {
|
||||
return &SpanMapper{
|
||||
Name: req.Name,
|
||||
FieldContext: req.FieldContext,
|
||||
Config: req.Config,
|
||||
Enabled: req.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpanMapperFromUpdatable(req *UpdatableSpanMapper) *SpanMapper {
|
||||
m := &SpanMapper{}
|
||||
if req.FieldContext != (FieldContext{}) {
|
||||
m.FieldContext = req.FieldContext
|
||||
}
|
||||
if req.Config != nil {
|
||||
m.Config = *req.Config
|
||||
}
|
||||
if req.Enabled != nil {
|
||||
m.Enabled = *req.Enabled
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func NewSpanMappersFromStorableSpanMappers(ss []*StorableSpanMapper) []*SpanMapper {
|
||||
mappers := make([]*SpanMapper, len(ss))
|
||||
for i, s := range ss {
|
||||
mappers[i] = NewSpanMapperFromStorable(s)
|
||||
}
|
||||
return mappers
|
||||
}
|
||||
|
||||
func NewGettableSpanMappers(m []*SpanMapper) *GettableSpanMappers {
|
||||
return &GettableSpanMappers{Items: m}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package spantypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeMappingGroupNotFound = errors.MustNewCode("span_attribute_mapping_group_not_found")
|
||||
ErrCodeMappingGroupAlreadyExists = errors.MustNewCode("span_attribute_mapping_group_already_exists")
|
||||
)
|
||||
|
||||
// SpanMapperGroupCategory defaults will be llm, tool, agent but user can configure more as they want.
|
||||
type SpanMapperGroupCategory valuer.String
|
||||
|
||||
// A group runs when any of the listed attribute/resource key patterns match.
|
||||
type SpanMapperGroupCondition struct {
|
||||
Attributes []string `json:"attributes" required:"true" nullable:"true"`
|
||||
Resource []string `json:"resource" required:"true" nullable:"true"`
|
||||
}
|
||||
|
||||
// SpanMapperGroup is the domain model for a span attribute mapping group.
|
||||
type SpanMapperGroup struct {
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
|
||||
ID valuer.UUID `json:"id" required:"true"`
|
||||
OrgID valuer.UUID `json:"orgId" required:"true"`
|
||||
Name string `json:"name" required:"true"`
|
||||
Category SpanMapperGroupCategory `json:"category" required:"true"`
|
||||
Condition SpanMapperGroupCondition `json:"condition" required:"true"`
|
||||
Enabled bool `json:"enabled" required:"true"`
|
||||
}
|
||||
|
||||
// GettableSpanMapperGroup is the HTTP response representation of a mapping group.
|
||||
type GettableSpanMapperGroup = SpanMapperGroup
|
||||
|
||||
type PostableSpanMapperGroup struct {
|
||||
Name string `json:"name" required:"true"`
|
||||
Category SpanMapperGroupCategory `json:"category" required:"true"`
|
||||
Condition SpanMapperGroupCondition `json:"condition" required:"true"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// UpdatableSpanMapperGroup is the HTTP request body for updating a mapping group.
|
||||
// All fields are optional; only non-nil fields are applied.
|
||||
type UpdatableSpanMapperGroup struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Condition *SpanMapperGroupCondition `json:"condition,omitempty"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
}
|
||||
|
||||
type ListSpanMapperGroupsQuery struct {
|
||||
Category *SpanMapperGroupCategory `query:"category"`
|
||||
Enabled *bool `query:"enabled"`
|
||||
}
|
||||
|
||||
type GettableSpanMapperGroups struct {
|
||||
Items []*GettableSpanMapperGroup `json:"items" required:"true" nullable:"false"`
|
||||
}
|
||||
|
||||
func NewSpanMapperGroupFromStorable(s *StorableSpanMapperGroup) *SpanMapperGroup {
|
||||
return &SpanMapperGroup{
|
||||
TimeAuditable: s.TimeAuditable,
|
||||
UserAuditable: s.UserAuditable,
|
||||
ID: s.ID,
|
||||
OrgID: s.OrgID,
|
||||
Name: s.Name,
|
||||
Category: s.Category,
|
||||
Condition: s.Condition,
|
||||
Enabled: s.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpanMapperGroupFromPostable(p *PostableSpanMapperGroup) *SpanMapperGroup {
|
||||
return &SpanMapperGroup{
|
||||
Name: p.Name,
|
||||
Category: p.Category,
|
||||
Condition: p.Condition,
|
||||
Enabled: p.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpanMapperGroupFromUpdatable(u *UpdatableSpanMapperGroup) *SpanMapperGroup {
|
||||
g := &SpanMapperGroup{}
|
||||
if u.Name != nil {
|
||||
g.Name = *u.Name
|
||||
}
|
||||
if u.Condition != nil {
|
||||
g.Condition = *u.Condition
|
||||
}
|
||||
if u.Enabled != nil {
|
||||
g.Enabled = *u.Enabled
|
||||
}
|
||||
return g
|
||||
}
|
||||
|
||||
func NewSpanMapperGroupsFromStorableGroups(ss []*StorableSpanMapperGroup) []*SpanMapperGroup {
|
||||
groups := make([]*SpanMapperGroup, len(ss))
|
||||
for i, s := range ss {
|
||||
groups[i] = NewSpanMapperGroupFromStorable(s)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
func NewGettableSpanMapperGroups(g []*SpanMapperGroup) *GettableSpanMapperGroups {
|
||||
return &GettableSpanMapperGroups{Items: g}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package spantypes
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type StorableSpanMapperGroup struct {
|
||||
bun.BaseModel `bun:"table:span_mapper_group,alias:span_attribute_mapping_group"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
|
||||
OrgID valuer.UUID `bun:"org_id,type:text,notnull"`
|
||||
Name string `bun:"name,type:text,notnull"`
|
||||
Category SpanMapperGroupCategory `bun:"category,type:text,notnull"`
|
||||
Condition SpanMapperGroupCondition `bun:"condition,type:jsonb,notnull"`
|
||||
Enabled bool `bun:"enabled,notnull,default:true"`
|
||||
}
|
||||
|
||||
type StorableSpanMapper struct {
|
||||
bun.BaseModel `bun:"table:span_mapper,alias:span_attribute_mapping"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
|
||||
GroupID valuer.UUID `bun:"group_id,type:text,notnull"`
|
||||
Name string `bun:"name,type:text,notnull"`
|
||||
FieldContext FieldContext `bun:"field_context,type:text,notnull"`
|
||||
Config SpanMapperConfig `bun:"config,type:jsonb,notnull"`
|
||||
Enabled bool `bun:"enabled,notnull,default:true"`
|
||||
}
|
||||
|
||||
func (c SpanMapperGroupCondition) Value() (driver.Value, error) {
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (c *SpanMapperGroupCondition) Scan(src any) error {
|
||||
var raw []byte
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
raw = []byte(v)
|
||||
case []byte:
|
||||
raw = v
|
||||
case nil:
|
||||
*c = SpanMapperGroupCondition{}
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInternalf(errors.CodeInternal, "spanmapper: cannot scan %T into Condition", src)
|
||||
}
|
||||
return json.Unmarshal(raw, c)
|
||||
}
|
||||
|
||||
func (m SpanMapperConfig) Value() (driver.Value, error) {
|
||||
b, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (m *SpanMapperConfig) Scan(src any) error {
|
||||
var raw []byte
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
raw = []byte(v)
|
||||
case []byte:
|
||||
raw = v
|
||||
case nil:
|
||||
*m = SpanMapperConfig{}
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInternalf(errors.CodeInternal, "spanmapper: cannot scan %T into MapperConfig", src)
|
||||
}
|
||||
return json.Unmarshal(raw, m)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package spantypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
// Group operations
|
||||
ListSpanMapperGroups(ctx context.Context, orgID valuer.UUID, q *ListSpanMapperGroupsQuery) ([]*StorableSpanMapperGroup, error)
|
||||
GetSpanMapperGroup(ctx context.Context, orgID, id valuer.UUID) (*StorableSpanMapperGroup, error)
|
||||
CreateSpanMapperGroup(ctx context.Context, group *StorableSpanMapperGroup) error
|
||||
UpdateSpanMapperGroup(ctx context.Context, group *StorableSpanMapperGroup) error
|
||||
DeleteSpanMapperGroup(ctx context.Context, orgID, id valuer.UUID) error
|
||||
|
||||
// Mapper operations
|
||||
ListSpanMappers(ctx context.Context, orgID, groupID valuer.UUID) ([]*StorableSpanMapper, error)
|
||||
GetSpanMapper(ctx context.Context, orgID, groupID, id valuer.UUID) (*StorableSpanMapper, error)
|
||||
CreateSpanMapper(ctx context.Context, mapper *StorableSpanMapper) error
|
||||
UpdateSpanMapper(ctx context.Context, mapper *StorableSpanMapper) error
|
||||
DeleteSpanMapper(ctx context.Context, orgID, groupID, id valuer.UUID) error
|
||||
}
|
||||
Reference in New Issue
Block a user