mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-22 06:00:29 +01:00
Compare commits
4 Commits
issue_5267
...
feat/gcp-c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f3fe7ec6e | ||
|
|
4f45b3a691 | ||
|
|
dc4b4c3d03 | ||
|
|
c5f550d88f |
1
.github/workflows/integrationci.yaml
vendored
1
.github/workflows/integrationci.yaml
vendored
@@ -58,7 +58,6 @@ jobs:
|
||||
- role
|
||||
- rootuser
|
||||
- serviceaccount
|
||||
- spanmapper
|
||||
- querier_json_body
|
||||
- querier_skip_resource_fingerprint
|
||||
- ttl
|
||||
|
||||
@@ -9,11 +9,6 @@ global:
|
||||
# the path component (e.g. /signoz in https://example.com/signoz) is used
|
||||
# as the base path for all HTTP routes (both API and web frontend).
|
||||
external_url: <unset>
|
||||
# origins (scheme://host[:port], no path) allowed as login redirect targets. include
|
||||
# the origin the signoz ui is served on. when not configured, redirect targets are
|
||||
# not validated.
|
||||
# allowed_origins:
|
||||
# - https://signoz.example.com
|
||||
# the url where the SigNoz backend receives telemetry data (traces, metrics, logs) from instrumented applications.
|
||||
ingestion_url: <unset>
|
||||
# the url of the SigNoz MCP server. when unset, the MCP settings page is hidden in the frontend.
|
||||
|
||||
@@ -8084,19 +8084,6 @@ components:
|
||||
required:
|
||||
- items
|
||||
type: object
|
||||
SpantypesGettableSpanMapperTest:
|
||||
properties:
|
||||
collectorLogs:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
spans:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperTestSpan'
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
SpantypesGettableSpanMappers:
|
||||
properties:
|
||||
items:
|
||||
@@ -8193,39 +8180,6 @@ components:
|
||||
- name
|
||||
- condition
|
||||
type: object
|
||||
SpantypesPostableSpanMapperTest:
|
||||
properties:
|
||||
groups:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesPostableSpanMapperTestGroup'
|
||||
nullable: true
|
||||
type: array
|
||||
spans:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperTestSpan'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- spans
|
||||
- groups
|
||||
type: object
|
||||
SpantypesPostableSpanMapperTestGroup:
|
||||
properties:
|
||||
condition:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroupCondition'
|
||||
enabled:
|
||||
type: boolean
|
||||
mappers:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesPostableSpanMapper'
|
||||
nullable: true
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- condition
|
||||
type: object
|
||||
SpantypesPostableTraceAggregations:
|
||||
properties:
|
||||
aggregations:
|
||||
@@ -8387,17 +8341,6 @@ components:
|
||||
- operation
|
||||
- priority
|
||||
type: object
|
||||
SpantypesSpanMapperTestSpan:
|
||||
properties:
|
||||
attributes:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
resource:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
type: object
|
||||
SpantypesUpdatableSpanMapper:
|
||||
properties:
|
||||
config:
|
||||
@@ -8615,7 +8558,6 @@ components:
|
||||
- resource
|
||||
- attribute
|
||||
- body
|
||||
- ""
|
||||
type: string
|
||||
TelemetrytypesFieldDataType:
|
||||
enum:
|
||||
@@ -8624,7 +8566,6 @@ components:
|
||||
- float64
|
||||
- int64
|
||||
- number
|
||||
- ""
|
||||
type: string
|
||||
TelemetrytypesGettableFieldKeys:
|
||||
properties:
|
||||
@@ -8656,7 +8597,6 @@ components:
|
||||
- traces
|
||||
- logs
|
||||
- metrics
|
||||
- ""
|
||||
type: string
|
||||
TelemetrytypesSource:
|
||||
enum:
|
||||
@@ -14131,69 +14071,6 @@ paths:
|
||||
summary: Update a span mapper
|
||||
tags:
|
||||
- spanmapper
|
||||
/api/v1/span_mapper_groups/test:
|
||||
post:
|
||||
deprecated: false
|
||||
description: Tests how span mappers would transform sample spans
|
||||
operationId: TestSpanMappers
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpantypesPostableSpanMapperTest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesGettableSpanMapperTest'
|
||||
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: Test span mappers against sample spans
|
||||
tags:
|
||||
- spanmapper
|
||||
/api/v1/stats:
|
||||
get:
|
||||
deprecated: false
|
||||
|
||||
@@ -223,12 +223,17 @@ func (provider *provider) Update(ctx context.Context, orgID valuer.UUID, updated
|
||||
return err
|
||||
}
|
||||
|
||||
desiredTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, updatedRole.TransactionGroups)
|
||||
existingGroups := authtypes.MustNewTransactionGroupsFromTuples(existingTuples)
|
||||
additions, deletions := existingGroups.Diff(updatedRole.TransactionGroups)
|
||||
additionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, additions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
additionTuples, deletionTuples := authtypes.DiffTuples(existingTuples, desiredTuples)
|
||||
deletionTuples, err := authtypes.NewTuplesFromTransactionGroups(existingRole.Name, orgID, deletions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = provider.Write(ctx, additionTuples, deletionTuples)
|
||||
if err != nil {
|
||||
|
||||
@@ -240,17 +240,17 @@ func TestManager_TestNotification_SendUnmatched_PromRule(t *testing.T) {
|
||||
mock := mockStore.Mock()
|
||||
|
||||
// Mock the fingerprint query (for Prometheus label matching)
|
||||
// args: $1=metric_name (the __name__ matcher maps onto the column)
|
||||
mock.ExpectQuery("SELECT fingerprint, any").
|
||||
WithArgs("test_metric").
|
||||
WithArgs("test_metric", "__name__", "test_metric").
|
||||
WillReturnRows(fingerprintRows)
|
||||
|
||||
// Mock the samples query (for Prometheus metric data)
|
||||
// args: metric_name IN (discovered names), subquery metric_name, start, end
|
||||
mock.ExpectQuery("SELECT metric_name, fingerprint, unix_milli").
|
||||
WithArgs(
|
||||
"test_metric",
|
||||
"test_metric",
|
||||
"__name__",
|
||||
"test_metric",
|
||||
queryStart,
|
||||
queryEnd,
|
||||
).
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
"lodash-es": "^4.17.21",
|
||||
"motion": "12.4.13",
|
||||
"nuqs": "2.8.8",
|
||||
"overlayscrollbars": "^2.16.0",
|
||||
"overlayscrollbars": "^2.8.1",
|
||||
"overlayscrollbars-react": "^0.5.6",
|
||||
"papaparse": "5.4.1",
|
||||
"posthog-js": "1.298.0",
|
||||
@@ -221,18 +221,5 @@
|
||||
"*.(scss|css)": [
|
||||
"stylelint"
|
||||
]
|
||||
},
|
||||
"overrides": {
|
||||
"@babel/core@<=7.29.0": ">=7.29.6 <8",
|
||||
"@istanbuljs/load-nyc-config>js-yaml": ">=4.2.0 <5",
|
||||
"cookie@<0.7.0": ">=0.7.1 <1",
|
||||
"dompurify@<=3.4.10": ">=3.4.11 <4",
|
||||
"esbuild@>=0.27.3 <0.28.1": ">=0.28.1 <0.29.0",
|
||||
"js-cookie@<=3.0.5": ">=3.0.7 <4",
|
||||
"js-yaml@>=4.0.0 <=4.1.1": ">=4.2.0 <5",
|
||||
"prismjs@<1.30.0": ">=1.30.0 <2",
|
||||
"react-router@>=6.7.0 <6.30.4": ">=6.30.4 <7",
|
||||
"tmp@<0.2.6": ">=0.2.6 <0.3.0",
|
||||
"yaml@>=1.0.0 <1.10.3": ">=1.10.3 <2"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
frontend/pnpm-lock.yaml
generated
16
frontend/pnpm-lock.yaml
generated
@@ -187,11 +187,11 @@ importers:
|
||||
specifier: 2.8.8
|
||||
version: 2.8.8(react-router-dom@5.3.4(react@18.2.0))(react-router@6.30.4(react@18.2.0))(react@18.2.0)
|
||||
overlayscrollbars:
|
||||
specifier: ^2.16.0
|
||||
version: 2.16.0
|
||||
specifier: ^2.8.1
|
||||
version: 2.9.2
|
||||
overlayscrollbars-react:
|
||||
specifier: ^0.5.6
|
||||
version: 0.5.6(overlayscrollbars@2.16.0)(react@18.2.0)
|
||||
version: 0.5.6(overlayscrollbars@2.9.2)(react@18.2.0)
|
||||
papaparse:
|
||||
specifier: 5.4.1
|
||||
version: 5.4.1
|
||||
@@ -6941,8 +6941,8 @@ packages:
|
||||
overlayscrollbars: ^2.0.0
|
||||
react: '>=16.8.0'
|
||||
|
||||
overlayscrollbars@2.16.0:
|
||||
resolution: {integrity: sha512-N03oje/q7j93D0aLZtoCdsDSYLmhheSsv8H7oSLE7HhdV9P/bmCURtLV/KbPye7P/bpfyt/obSfDpGUYoJ0OWg==}
|
||||
overlayscrollbars@2.9.2:
|
||||
resolution: {integrity: sha512-iDT84r39i7oWP72diZN2mbJUsn/taCq568aQaIrc84S87PunBT7qtsVltAF2esk7ORTRjQDnfjVYoqqTzgs8QA==}
|
||||
|
||||
oxfmt@0.54.0:
|
||||
resolution: {integrity: sha512-DjnMwn7smSLF+Mc2+pRItnuPftm/dkUFpY/d4+33y9TfKrsHZo8GLhmUg9BrOIUEy94Rlom1Q11N6vuhE+e0oQ==}
|
||||
@@ -16456,12 +16456,12 @@ snapshots:
|
||||
|
||||
outvariant@1.4.0: {}
|
||||
|
||||
overlayscrollbars-react@0.5.6(overlayscrollbars@2.16.0)(react@18.2.0):
|
||||
overlayscrollbars-react@0.5.6(overlayscrollbars@2.9.2)(react@18.2.0):
|
||||
dependencies:
|
||||
overlayscrollbars: 2.16.0
|
||||
overlayscrollbars: 2.9.2
|
||||
react: 18.2.0
|
||||
|
||||
overlayscrollbars@2.16.0: {}
|
||||
overlayscrollbars@2.9.2: {}
|
||||
|
||||
oxfmt@0.54.0:
|
||||
dependencies:
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
|
||||
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
|
||||
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
|
||||
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
|
||||
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
|
||||
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
|
||||
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
|
||||
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration",
|
||||
"LLM_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | LLM Observability Attribute Mapping"
|
||||
}
|
||||
@@ -87,7 +87,7 @@
|
||||
"TRACES_FUNNELS_DETAIL": "SigNoz | Funnel",
|
||||
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
|
||||
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
|
||||
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
|
||||
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
|
||||
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
|
||||
"LLM_OBSERVABILITY_OVERVIEW": "SigNoz | LLM Observability Overview",
|
||||
"LLM_OBSERVABILITY_CONFIGURATION": "SigNoz | LLM Observability Configuration",
|
||||
"LLM_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | LLM Observability Attribute Mapping"
|
||||
}
|
||||
@@ -134,8 +134,8 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
|
||||
}
|
||||
|
||||
if (
|
||||
(pathname.startsWith(`${ROUTES.AI_OBSERVABILITY_BASE}/`) ||
|
||||
pathname === ROUTES.AI_OBSERVABILITY_BASE) &&
|
||||
(pathname.startsWith(`${ROUTES.LLM_OBSERVABILITY_BASE}/`) ||
|
||||
pathname === ROUTES.LLM_OBSERVABILITY_BASE) &&
|
||||
!isAIObservabilityEnabled
|
||||
) {
|
||||
return <Redirect to={ROUTES.HOME} />;
|
||||
|
||||
@@ -514,24 +514,24 @@ const routes: AppRoutes[] = [
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING,
|
||||
path: ROUTES.LLM_OBSERVABILITY_ATTRIBUTE_MAPPING,
|
||||
exact: true,
|
||||
component: LLMObservabilityPage,
|
||||
key: 'AI_OBSERVABILITY_ATTRIBUTE_MAPPING',
|
||||
key: 'LLM_OBSERVABILITY_ATTRIBUTE_MAPPING',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.AI_OBSERVABILITY_OVERVIEW,
|
||||
path: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
|
||||
exact: true,
|
||||
component: LLMObservabilityPage,
|
||||
key: 'AI_OBSERVABILITY_OVERVIEW',
|
||||
key: 'LLM_OBSERVABILITY_OVERVIEW',
|
||||
isPrivate: true,
|
||||
},
|
||||
{
|
||||
path: ROUTES.AI_OBSERVABILITY_CONFIGURATION,
|
||||
path: ROUTES.LLM_OBSERVABILITY_CONFIGURATION,
|
||||
exact: true,
|
||||
component: LLMObservabilityPage,
|
||||
key: 'AI_OBSERVABILITY_CONFIGURATION',
|
||||
key: 'LLM_OBSERVABILITY_CONFIGURATION',
|
||||
isPrivate: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
|
||||
export function mockFieldsAPIsWithEmptyResponse(): void {
|
||||
server.use(
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: { keys: {}, complete: true } }),
|
||||
),
|
||||
),
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/values`, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: { values: {}, complete: true } }),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -3479,7 +3479,6 @@ export enum TelemetrytypesFieldContextDTO {
|
||||
resource = 'resource',
|
||||
attribute = 'attribute',
|
||||
body = 'body',
|
||||
'' = '',
|
||||
}
|
||||
export enum TelemetrytypesFieldDataTypeDTO {
|
||||
string = 'string',
|
||||
@@ -3487,13 +3486,11 @@ export enum TelemetrytypesFieldDataTypeDTO {
|
||||
float64 = 'float64',
|
||||
int64 = 'int64',
|
||||
number = 'number',
|
||||
'' = '',
|
||||
}
|
||||
export enum TelemetrytypesSignalDTO {
|
||||
traces = 'traces',
|
||||
logs = 'logs',
|
||||
metrics = 'metrics',
|
||||
'' = '',
|
||||
}
|
||||
export interface Querybuildertypesv5GroupByKeyDTO {
|
||||
/**
|
||||
@@ -9270,48 +9267,6 @@ export interface SpantypesGettableSpanMapperGroupsDTO {
|
||||
items: SpantypesSpanMapperGroupDTO[];
|
||||
}
|
||||
|
||||
export type SpantypesSpanMapperTestSpanDTOAttributesAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanMapperTestSpanDTOAttributes =
|
||||
SpantypesSpanMapperTestSpanDTOAttributesAnyOf | null;
|
||||
|
||||
export type SpantypesSpanMapperTestSpanDTOResourceAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanMapperTestSpanDTOResource =
|
||||
SpantypesSpanMapperTestSpanDTOResourceAnyOf | null;
|
||||
|
||||
export interface SpantypesSpanMapperTestSpanDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
attributes?: SpantypesSpanMapperTestSpanDTOAttributes;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
resource?: SpantypesSpanMapperTestSpanDTOResource;
|
||||
}
|
||||
|
||||
export interface SpantypesGettableSpanMapperTestDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
collectorLogs?: string[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
spans?: SpantypesSpanMapperTestSpanDTO[] | null;
|
||||
}
|
||||
|
||||
export enum SpantypesSpanMapperOperationDTO {
|
||||
move = 'move',
|
||||
copy = 'copy',
|
||||
@@ -9653,33 +9608,6 @@ export interface SpantypesPostableSpanMapperGroupDTO {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SpantypesPostableSpanMapperTestGroupDTO {
|
||||
condition: SpantypesSpanMapperGroupConditionDTO | null;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
mappers?: SpantypesPostableSpanMapperDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SpantypesPostableSpanMapperTestDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
groups: SpantypesPostableSpanMapperTestGroupDTO[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
spans: SpantypesSpanMapperTestSpanDTO[] | null;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanAggregationDTO {
|
||||
aggregation: SpantypesSpanAggregationTypeDTO;
|
||||
field: TelemetrytypesTelemetryFieldKeyDTO;
|
||||
@@ -11030,14 +10958,6 @@ export type UpdateSpanMapperPathParameters = {
|
||||
groupId: string;
|
||||
mapperId: string;
|
||||
};
|
||||
export type TestSpanMappers200 = {
|
||||
data: SpantypesGettableSpanMapperTestDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetStats200Data = { [key: string]: unknown };
|
||||
|
||||
export type GetStats200 = {
|
||||
|
||||
@@ -30,10 +30,8 @@ import type {
|
||||
RenderErrorResponseDTO,
|
||||
SpantypesPostableSpanMapperDTO,
|
||||
SpantypesPostableSpanMapperGroupDTO,
|
||||
SpantypesPostableSpanMapperTestDTO,
|
||||
SpantypesUpdatableSpanMapperDTO,
|
||||
SpantypesUpdatableSpanMapperGroupDTO,
|
||||
TestSpanMappers200,
|
||||
UpdateSpanMapperGroupPathParameters,
|
||||
UpdateSpanMapperPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
@@ -782,86 +780,3 @@ export const useUpdateSpanMapper = <
|
||||
> => {
|
||||
return useMutation(getUpdateSpanMapperMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Tests how span mappers would transform sample spans
|
||||
* @summary Test span mappers against sample spans
|
||||
*/
|
||||
export const testSpanMappers = (
|
||||
spantypesPostableSpanMapperTestDTO?: BodyType<SpantypesPostableSpanMapperTestDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<TestSpanMappers200>({
|
||||
url: `/api/v1/span_mapper_groups/test`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: spantypesPostableSpanMapperTestDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getTestSpanMappersMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['testSpanMappers'];
|
||||
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 testSpanMappers>>,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return testSpanMappers(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type TestSpanMappersMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>
|
||||
>;
|
||||
export type TestSpanMappersMutationBody =
|
||||
| BodyType<SpantypesPostableSpanMapperTestDTO>
|
||||
| undefined;
|
||||
export type TestSpanMappersMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Test span mappers against sample spans
|
||||
*/
|
||||
export const useTestSpanMappers = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getTestSpanMappersMutationOptions(options));
|
||||
};
|
||||
|
||||
1
frontend/src/assets/Logos/gcp.svg
Normal file
1
frontend/src/assets/Logos/gcp.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 128 128"><path fill="#ea4535" d="M80.6 40.3h.4l-.2-.2 14-14v-.3c-11.8-10.4-28.1-14-43.2-9.5C36.5 20.8 24.9 32.8 20.7 48c.2-.1.5-.2.8-.2 5.2-3.4 11.4-5.4 17.9-5.4 2.2 0 4.3.2 6.4.6.1-.1.2-.1.3-.1 9-9.9 24.2-11.1 34.6-2.6h-.1z"/><path fill="#557ebf" d="M108.1 47.8c-2.3-8.5-7.1-16.2-13.8-22.1L80 39.9c6 4.9 9.5 12.3 9.3 20v2.5c16.9 0 16.9 25.2 0 25.2H63.9v20h-.1l.1.2h25.4c14.6.1 27.5-9.3 31.8-23.1 4.3-13.8-1-28.8-13-36.9z"/><path fill="#36a852" d="M39 107.9h26.3V87.7H39c-1.9 0-3.7-.4-5.4-1.1l-15.2 14.6v.2c6 4.3 13.2 6.6 20.7 6.6z"/><path fill="#f9bc15" d="M40.2 41.9c-14.9.1-28.1 9.3-32.9 22.8-4.8 13.6 0 28.5 11.8 37.3l15.6-14.9c-8.6-3.7-10.6-14.5-4-20.8 6.6-6.4 17.8-4.4 21.7 3.8L68 55.2C61.4 46.9 51.1 42 40.2 42.1z"/></svg>
|
||||
|
After Width: | Height: | Size: 805 B |
@@ -20,7 +20,7 @@ import { Globe, Inbox, SquarePen } from '@signozhq/icons';
|
||||
|
||||
import AnnouncementsModal from './AnnouncementsModal';
|
||||
import FeedbackModal from './FeedbackModal';
|
||||
import ShareURLModal, { type ShareURLExtraOption } from './ShareURLModal';
|
||||
import ShareURLModal from './ShareURLModal';
|
||||
|
||||
import './HeaderRightSection.styles.scss';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
@@ -29,15 +29,12 @@ interface HeaderRightSectionProps {
|
||||
enableAnnouncements: boolean;
|
||||
enableShare: boolean;
|
||||
enableFeedback: boolean;
|
||||
/** Optional page-specific toggle for the share dialog (e.g. "Include variables"). */
|
||||
shareModalExtraOption?: ShareURLExtraOption;
|
||||
}
|
||||
|
||||
function HeaderRightSection({
|
||||
enableAnnouncements,
|
||||
enableShare,
|
||||
enableFeedback,
|
||||
shareModalExtraOption,
|
||||
}: HeaderRightSectionProps): JSX.Element | null {
|
||||
const location = useLocation();
|
||||
|
||||
@@ -188,7 +185,7 @@ function HeaderRightSection({
|
||||
rootClassName="header-section-popover-root"
|
||||
className="shareable-link-popover"
|
||||
placement="bottomRight"
|
||||
content={<ShareURLModal extraOption={shareModalExtraOption} />}
|
||||
content={<ShareURLModal />}
|
||||
open={openShareURLModal}
|
||||
destroyTooltipOnHide
|
||||
arrow={false}
|
||||
|
||||
@@ -24,22 +24,7 @@ const routesToBeSharedWithTime = [
|
||||
ROUTES.METER_EXPLORER,
|
||||
];
|
||||
|
||||
/**
|
||||
* An optional, page-specific toggle in the share dialog (e.g. a dashboard's
|
||||
* "Include variables"). When enabled, `apply` mutates the URL params that go into
|
||||
* the shared link. Keeps this shared modal generic — the page owns what it adds.
|
||||
*/
|
||||
export interface ShareURLExtraOption {
|
||||
label: string;
|
||||
defaultEnabled?: boolean;
|
||||
apply: (params: URLSearchParams) => void;
|
||||
}
|
||||
|
||||
interface ShareURLModalProps {
|
||||
extraOption?: ShareURLExtraOption;
|
||||
}
|
||||
|
||||
function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
|
||||
function ShareURLModal(): JSX.Element {
|
||||
const urlQuery = useUrlQuery();
|
||||
const location = useLocation();
|
||||
const { selectedTime } = useSelector<AppState, GlobalReducer>(
|
||||
@@ -49,9 +34,6 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
|
||||
const [enableAbsoluteTime, setEnableAbsoluteTime] = useState(
|
||||
selectedTime !== 'custom',
|
||||
);
|
||||
const [enableExtraOption, setEnableExtraOption] = useState(
|
||||
extraOption?.defaultEnabled ?? false,
|
||||
);
|
||||
|
||||
const startTime = urlQuery.get(QueryParams.startTime);
|
||||
const endTime = urlQuery.get(QueryParams.endTime);
|
||||
@@ -111,11 +93,6 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
if (extraOption && enableExtraOption) {
|
||||
extraOption.apply(urlQuery);
|
||||
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
|
||||
}
|
||||
|
||||
return currentUrl;
|
||||
};
|
||||
|
||||
@@ -166,20 +143,6 @@ function ShareURLModal({ extraOption }: ShareURLModalProps): JSX.Element {
|
||||
</>
|
||||
)}
|
||||
|
||||
{extraOption && (
|
||||
<div className="absolute-relative-time-toggler-container">
|
||||
<Typography.Text className="absolute-relative-time-toggler-label">
|
||||
{extraOption.label}
|
||||
</Typography.Text>
|
||||
<div className="absolute-relative-time-toggler">
|
||||
<Switch
|
||||
value={enableExtraOption}
|
||||
onChange={(): void => setEnableExtraOption((prev) => !prev)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="share-link">
|
||||
<div className="url-share-container">
|
||||
<div className="url-share-container-header">
|
||||
|
||||
@@ -247,6 +247,11 @@
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
|
||||
&,
|
||||
.ͼ1a {
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground));
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
|
||||
@@ -293,6 +293,11 @@ $max-recents-shown: 5;
|
||||
var(--l2-background)
|
||||
) !important;
|
||||
|
||||
&,
|
||||
.ͼ1a {
|
||||
color: var(--query-builder-v2-color, var(--l2-foreground));
|
||||
}
|
||||
|
||||
::-moz-selection {
|
||||
background: var(
|
||||
--query-builder-v2-selection-background-color,
|
||||
|
||||
@@ -89,10 +89,10 @@ const ROUTES = {
|
||||
AI_ASSISTANT_BASE: '/ai-assistant',
|
||||
AI_ASSISTANT_ICON_PREVIEW: '/ai-assistant-icon-preview',
|
||||
MCP_SERVER: '/settings/mcp-server',
|
||||
AI_OBSERVABILITY_ATTRIBUTE_MAPPING: '/ai-observability/attribute-mapping',
|
||||
AI_OBSERVABILITY_BASE: '/ai-observability',
|
||||
AI_OBSERVABILITY_OVERVIEW: '/ai-observability/overview',
|
||||
AI_OBSERVABILITY_CONFIGURATION: '/ai-observability/configuration',
|
||||
LLM_OBSERVABILITY_ATTRIBUTE_MAPPING: '/llm-observability/attribute-mapping',
|
||||
LLM_OBSERVABILITY_BASE: '/llm-observability',
|
||||
LLM_OBSERVABILITY_OVERVIEW: '/llm-observability/overview',
|
||||
LLM_OBSERVABILITY_CONFIGURATION: '/llm-observability/configuration',
|
||||
} as const;
|
||||
|
||||
export default ROUTES;
|
||||
|
||||
@@ -1,39 +1,148 @@
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { Color, Spacing } from '@signozhq/design-tokens';
|
||||
import { X } from '@signozhq/icons';
|
||||
import { Button, Drawer, Tooltip } from 'antd';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { Divider } from '@signozhq/ui/divider';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Drawer } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ErrorContent from 'components/ErrorModal/components/ErrorContent';
|
||||
import APIError from 'types/api/error';
|
||||
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
GlobalTimeProvider,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
useGlobalTimeStore,
|
||||
} from 'store/globalTime';
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import GetMinMax from 'lib/getMinMax';
|
||||
import {
|
||||
BarChart,
|
||||
ChevronsLeftRight,
|
||||
Compass,
|
||||
DraftingCompass,
|
||||
ScrollText,
|
||||
X,
|
||||
} from '@signozhq/icons';
|
||||
import { isCustomTimeRange, useGlobalTimeStore } from 'store/globalTime';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime/utils';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { INFRA_MONITORING_K8S_PARAMS_KEYS } from '../constants';
|
||||
import { useInfraMonitoringSelectedItemParams } from '../hooks';
|
||||
import { InfraMonitoringEntity, VIEW_TYPES } from '../constants';
|
||||
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
|
||||
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
|
||||
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
|
||||
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
|
||||
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
|
||||
import {
|
||||
SelectedItemParams,
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringSelectedItemParams,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
import LoadingContainer from '../LoadingContainer';
|
||||
|
||||
import K8sBaseDetailsContent from './K8sBaseDetailsContent';
|
||||
import { K8sBaseDetailsProps } from './types';
|
||||
|
||||
import '../EntityDetailsUtils/entityDetails.styles.scss';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import {
|
||||
EntityCountConfig,
|
||||
EntityCountsSection,
|
||||
} from './components/EntityCountsSection/EntityCountsSection';
|
||||
|
||||
export type {
|
||||
CustomTab,
|
||||
CustomTabRenderProps,
|
||||
K8sBaseDetailsProps,
|
||||
K8sDetailsCountConfig,
|
||||
K8sDetailsFilters,
|
||||
K8sDetailsMetadataConfig,
|
||||
} from './types';
|
||||
const TimeRangeOffset = 1000000000;
|
||||
|
||||
export interface K8sDetailsMetadataConfig<T> {
|
||||
label: string;
|
||||
getValue: (entity: T) => string | number;
|
||||
render?: (value: string | number, entity: T) => React.ReactNode;
|
||||
}
|
||||
|
||||
export type K8sDetailsCountConfig<T> = EntityCountConfig<T>;
|
||||
|
||||
export interface K8sDetailsFilters {
|
||||
filter: { expression: string };
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface CustomTabRenderProps<T> {
|
||||
entity: T;
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: Time;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface CustomTab<T> {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
render: (props: CustomTabRenderProps<T>) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface K8sBaseDetailsProps<T> {
|
||||
category: InfraMonitoringEntity;
|
||||
eventCategory: string;
|
||||
// Data fetching configuration
|
||||
getSelectedItemExpression: (params: SelectedItemParams) => string;
|
||||
fetchEntityData: (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{ data: T | null; error?: APIError | null }>;
|
||||
// Entity configuration
|
||||
getEntityName: (entity: T) => string;
|
||||
getInitialLogTracesExpression: (entity: T) => string;
|
||||
getInitialEventsExpression: (entity: T) => string;
|
||||
metadataConfig: K8sDetailsMetadataConfig<T>[];
|
||||
countsConfig?: K8sDetailsCountConfig<T>[];
|
||||
getCountsFilterExpression?: (entity: T) => string;
|
||||
entityWidgetInfo: {
|
||||
title: string;
|
||||
yAxisUnit: string;
|
||||
docPath?: string;
|
||||
}[];
|
||||
getEntityQueryPayload: (
|
||||
entity: T,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
) => GetQueryResultsProps[];
|
||||
queryKeyPrefix: string;
|
||||
/** When true, only metrics are shown and the Metrics/Logs/Traces/Events tab bar is hidden. */
|
||||
hideDetailViewTabs?: boolean;
|
||||
tabsConfig?: {
|
||||
showMetrics?: boolean;
|
||||
showLogs?: boolean;
|
||||
showTraces?: boolean;
|
||||
showEvents?: boolean;
|
||||
};
|
||||
customTabs?: Array<CustomTab<T>>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function K8sBaseDetails<T>({
|
||||
category,
|
||||
eventCategory,
|
||||
@@ -54,6 +163,7 @@ export default function K8sBaseDetails<T>({
|
||||
}: K8sBaseDetailsProps<T>): JSX.Element {
|
||||
const selectedTime = useGlobalTimeStore((s) => s.selectedTime);
|
||||
const getMinMaxTime = useGlobalTimeStore((s) => s.getMinMaxTime);
|
||||
const lastComputedMinMax = useGlobalTimeStore((s) => s.lastComputedMinMax);
|
||||
const getAutoRefreshQueryKey = useGlobalTimeStore(
|
||||
(s) => s.getAutoRefreshQueryKey,
|
||||
);
|
||||
@@ -127,9 +237,86 @@ export default function K8sBaseDetails<T>({
|
||||
|
||||
const entityName = entity ? getEntityName(entity) : '';
|
||||
|
||||
// Content state (previously in K8sBaseDetailsContent)
|
||||
const tabVisibility = useMemo(
|
||||
() => ({
|
||||
showMetrics: true,
|
||||
showLogs: true,
|
||||
showTraces: true,
|
||||
showEvents: true,
|
||||
...tabsConfig,
|
||||
}),
|
||||
[tabsConfig],
|
||||
);
|
||||
|
||||
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,
|
||||
endTime: endMs,
|
||||
}));
|
||||
|
||||
// TODO(h4ad): Remove this and use context/zustand
|
||||
const lastSelectedInterval = useRef<Time | null>(null);
|
||||
const [selectedInterval, setSelectedInterval] = useState<Time>(
|
||||
lastSelectedInterval.current
|
||||
? lastSelectedInterval.current
|
||||
: isCustomTimeRange(selectedTime)
|
||||
? DEFAULT_TIME_RANGE
|
||||
: selectedTime,
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
|
||||
|
||||
const validTabs = useMemo(() => {
|
||||
const tabs: string[] = [];
|
||||
if (tabVisibility.showMetrics) {
|
||||
tabs.push(VIEW_TYPES.METRICS);
|
||||
}
|
||||
if (tabVisibility.showLogs) {
|
||||
tabs.push(VIEW_TYPES.LOGS);
|
||||
}
|
||||
if (tabVisibility.showTraces) {
|
||||
tabs.push(VIEW_TYPES.TRACES);
|
||||
}
|
||||
if (tabVisibility.showEvents) {
|
||||
tabs.push(VIEW_TYPES.EVENTS);
|
||||
}
|
||||
if (customTabs) {
|
||||
tabs.push(...customTabs.map((t) => t.key));
|
||||
}
|
||||
return tabs;
|
||||
}, [tabVisibility, customTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hideDetailViewTabs && !validTabs.includes(selectedView)) {
|
||||
const firstValid = validTabs[0] || VIEW_TYPES.METRICS;
|
||||
void setSelectedView(firstValid);
|
||||
}
|
||||
}, [hideDetailViewTabs, selectedView, validTabs, setSelectedView]);
|
||||
|
||||
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
|
||||
const [userLogsExpression] = useQueryState(
|
||||
K8S_ENTITY_LOGS_EXPRESSION_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
const [userTracesExpression] = useQueryState(
|
||||
K8S_ENTITY_TRACES_EXPRESSION_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (entity) {
|
||||
void logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
logEvent(InfraMonitoringEvents.PageVisited, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
@@ -137,6 +324,136 @@ export default function K8sBaseDetails<T>({
|
||||
}
|
||||
}, [entity, eventCategory]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSelectedInterval = lastSelectedInterval.current || selectedTime;
|
||||
if (!isCustomTimeRange(currentSelectedInterval)) {
|
||||
setSelectedInterval(currentSelectedInterval);
|
||||
const { minTime, maxTime } = getMinMaxTime();
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
}, [getMinMaxTime, selectedTime]);
|
||||
|
||||
const handleTabChange = (value: string | null): void => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
setSelectedView(value);
|
||||
setLogFiltersParam(null);
|
||||
setTracesFiltersParam(null);
|
||||
setEventsFiltersParam(null);
|
||||
logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
lastSelectedInterval.current = interval as Time;
|
||||
setSelectedInterval(interval as Time);
|
||||
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(dateTimeRange[0] / 1000),
|
||||
endTime: Math.floor(dateTimeRange[1] / 1000),
|
||||
});
|
||||
} else {
|
||||
const { maxTime, minTime } = GetMinMax(interval);
|
||||
|
||||
setModalTimeRange({
|
||||
startTime: Math.floor(minTime / TimeRangeOffset),
|
||||
endTime: Math.floor(maxTime / TimeRangeOffset),
|
||||
});
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
interval,
|
||||
view: effectiveView,
|
||||
});
|
||||
},
|
||||
[eventCategory, effectiveView],
|
||||
);
|
||||
|
||||
const handleExplorePagesRedirect = (): void => {
|
||||
const urlQuery = new URLSearchParams();
|
||||
|
||||
if (selectedInterval !== 'custom') {
|
||||
urlQuery.set(QueryParams.relativeTime, selectedInterval);
|
||||
} else {
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
urlQuery.set(QueryParams.startTime, modalTimeRange.startTime.toString());
|
||||
urlQuery.set(QueryParams.endTime, modalTimeRange.endTime.toString());
|
||||
}
|
||||
|
||||
logEvent(InfraMonitoringEvents.ExploreClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const fullExpression = combineInitialAndUserExpression(
|
||||
logsAndTracesInitialExpression,
|
||||
userLogsExpression || '',
|
||||
);
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.logs,
|
||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||
expression: fullExpression,
|
||||
filter: { expression: fullExpression },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const fullExpression = combineInitialAndUserExpression(
|
||||
logsAndTracesInitialExpression,
|
||||
userTracesExpression || '',
|
||||
);
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||
expression: fullExpression,
|
||||
filter: { expression: fullExpression },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
width="70%"
|
||||
@@ -181,33 +498,207 @@ export default function K8sBaseDetails<T>({
|
||||
</div>
|
||||
)}
|
||||
{entity && !isEntityLoading && !hasResponseError && (
|
||||
<GlobalTimeProvider
|
||||
inheritGlobalTime
|
||||
enableUrlParams={{
|
||||
relativeTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_RELATIVE_TIME,
|
||||
startTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_START_TIME,
|
||||
endTimeKey: INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_END_TIME,
|
||||
}}
|
||||
>
|
||||
<K8sBaseDetailsContent<T>
|
||||
entity={entity}
|
||||
category={category}
|
||||
eventCategory={eventCategory}
|
||||
metadataConfig={metadataConfig}
|
||||
countsConfig={countsConfig}
|
||||
getCountsFilterExpression={getCountsFilterExpression}
|
||||
selectedItem={selectedItem}
|
||||
handleClose={handleClose}
|
||||
entityWidgetInfo={entityWidgetInfo}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
queryKeyPrefix={queryKeyPrefix}
|
||||
hideDetailViewTabs={hideDetailViewTabs}
|
||||
tabsConfig={tabsConfig}
|
||||
customTabs={customTabs}
|
||||
logsAndTracesInitialExpression={logsAndTracesInitialExpression}
|
||||
eventsInitialExpression={eventsInitialExpression}
|
||||
/>
|
||||
</GlobalTimeProvider>
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
{metadataConfig.map((config) => (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
color="muted"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
{config.label}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
{metadataConfig.map((config) => {
|
||||
const value = config.getValue(entity);
|
||||
const displayValue = String(value);
|
||||
return (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
className="entity-details-metadata-value"
|
||||
>
|
||||
{config.render ? (
|
||||
config.render(value, entity)
|
||||
) : (
|
||||
<Tooltip title={displayValue}>{displayValue}</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{countsConfig &&
|
||||
countsConfig.length > 0 &&
|
||||
selectedItem &&
|
||||
getCountsFilterExpression && (
|
||||
<EntityCountsSection
|
||||
entity={entity}
|
||||
countsConfig={countsConfig}
|
||||
selectedItem={selectedItem}
|
||||
filterExpression={getCountsFilterExpression(entity)}
|
||||
closeDrawer={handleClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!hideDetailViewTabs && (
|
||||
<div className="views-tabs-container">
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
items={[
|
||||
...(tabVisibility.showMetrics
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.METRICS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<BarChart size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showLogs
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.LOGS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showTraces
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.TRACES,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<DraftingCompass size={14} />
|
||||
Traces
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showEvents
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.EVENTS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Events
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(customTabs?.map((tab) => ({
|
||||
value: tab.key,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</div>
|
||||
),
|
||||
})) ?? []),
|
||||
]}
|
||||
/>
|
||||
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<Tooltip title="Go to Logs Explorer" placement="left">
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<Tooltip title="Go to Traces Explorer" placement="left">
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{effectiveView === VIEW_TYPES.METRICS && (
|
||||
<EntityMetrics<T>
|
||||
entity={entity}
|
||||
selectedInterval={selectedInterval}
|
||||
timeRange={modalTimeRange}
|
||||
handleTimeChange={handleTimeChange}
|
||||
isModalTimeSelection
|
||||
entityWidgetInfo={entityWidgetInfo}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Metrics`}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.LOGS && (
|
||||
<EntityLogs
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey={`${queryKeyPrefix}Logs`}
|
||||
category={category}
|
||||
initialExpression={logsAndTracesInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.TRACES && (
|
||||
<EntityTraces
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey={`${queryKeyPrefix}Traces`}
|
||||
category={category}
|
||||
initialExpression={logsAndTracesInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.EVENTS && tabVisibility.showEvents && (
|
||||
<EntityEvents
|
||||
timeRange={modalTimeRange}
|
||||
isModalTimeSelection
|
||||
handleTimeChange={handleTimeChange}
|
||||
selectedInterval={selectedInterval}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Events`}
|
||||
initialExpression={eventsInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{customTabs?.map((tab) =>
|
||||
selectedView === tab.key ? (
|
||||
<React.Fragment key={tab.key}>
|
||||
{tab.render({
|
||||
entity,
|
||||
timeRange: modalTimeRange,
|
||||
selectedInterval,
|
||||
handleTimeChange,
|
||||
})}
|
||||
</React.Fragment>
|
||||
) : null,
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
@@ -1,401 +0,0 @@
|
||||
import { Fragment, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
BarChart,
|
||||
ChevronsLeftRight,
|
||||
Compass,
|
||||
DraftingCompass,
|
||||
ScrollText,
|
||||
} from '@signozhq/icons';
|
||||
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { combineInitialAndUserExpression } from 'components/QueryBuilderV2/QueryV2/QuerySearch/utils';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import {
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { parseAsString, useQueryState } from 'nuqs';
|
||||
import {
|
||||
LogsAggregatorOperator,
|
||||
TracesAggregatorOperator,
|
||||
} from 'types/common/queryBuilder';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import { VIEW_TYPES } from '../constants';
|
||||
import { useEntityDetailsTime } from '../EntityDetailsUtils/EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import EntityEvents from '../EntityDetailsUtils/EntityEvents';
|
||||
import EntityLogs from '../EntityDetailsUtils/EntityLogs';
|
||||
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityLogs/hooks';
|
||||
import EntityMetrics from '../EntityDetailsUtils/EntityMetrics';
|
||||
import EntityTraces from '../EntityDetailsUtils/EntityTraces';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../EntityDetailsUtils/EntityTraces/hooks';
|
||||
import {
|
||||
useInfraMonitoringEventsFilters,
|
||||
useInfraMonitoringLogFilters,
|
||||
useInfraMonitoringTracesFilters,
|
||||
useInfraMonitoringView,
|
||||
} from '../hooks';
|
||||
|
||||
import { EntityCountsSection } from './components/EntityCountsSection/EntityCountsSection';
|
||||
import { K8sBaseDetailsContentProps } from './types';
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export default function K8sBaseDetailsContent<T>({
|
||||
entity,
|
||||
category,
|
||||
eventCategory,
|
||||
metadataConfig,
|
||||
countsConfig,
|
||||
getCountsFilterExpression,
|
||||
selectedItem,
|
||||
handleClose,
|
||||
entityWidgetInfo,
|
||||
getEntityQueryPayload,
|
||||
queryKeyPrefix,
|
||||
hideDetailViewTabs,
|
||||
tabsConfig,
|
||||
customTabs,
|
||||
logsAndTracesInitialExpression,
|
||||
eventsInitialExpression,
|
||||
}: K8sBaseDetailsContentProps<T>): JSX.Element {
|
||||
const { timeRange, selectedInterval, handleTimeChange } =
|
||||
useEntityDetailsTime();
|
||||
|
||||
const tabVisibility = useMemo(
|
||||
() => ({
|
||||
showMetrics: true,
|
||||
showLogs: true,
|
||||
showTraces: true,
|
||||
showEvents: true,
|
||||
...tabsConfig,
|
||||
}),
|
||||
[tabsConfig],
|
||||
);
|
||||
|
||||
const [selectedView, setSelectedView] = useInfraMonitoringView();
|
||||
|
||||
const validTabs = useMemo(() => {
|
||||
const tabs: string[] = [];
|
||||
if (tabVisibility.showMetrics) {
|
||||
tabs.push(VIEW_TYPES.METRICS);
|
||||
}
|
||||
if (tabVisibility.showLogs) {
|
||||
tabs.push(VIEW_TYPES.LOGS);
|
||||
}
|
||||
if (tabVisibility.showTraces) {
|
||||
tabs.push(VIEW_TYPES.TRACES);
|
||||
}
|
||||
if (tabVisibility.showEvents) {
|
||||
tabs.push(VIEW_TYPES.EVENTS);
|
||||
}
|
||||
if (customTabs) {
|
||||
tabs.push(...customTabs.map((t) => t.key));
|
||||
}
|
||||
return tabs;
|
||||
}, [tabVisibility, customTabs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hideDetailViewTabs && !validTabs.includes(selectedView)) {
|
||||
const firstValid = validTabs[0] || VIEW_TYPES.METRICS;
|
||||
void setSelectedView(firstValid);
|
||||
}
|
||||
}, [hideDetailViewTabs, selectedView, validTabs, setSelectedView]);
|
||||
|
||||
const effectiveView = hideDetailViewTabs ? VIEW_TYPES.METRICS : selectedView;
|
||||
|
||||
const [, setLogFiltersParam] = useInfraMonitoringLogFilters();
|
||||
const [, setTracesFiltersParam] = useInfraMonitoringTracesFilters();
|
||||
const [, setEventsFiltersParam] = useInfraMonitoringEventsFilters();
|
||||
const [userLogsExpression] = useQueryState(
|
||||
K8S_ENTITY_LOGS_EXPRESSION_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
const [userTracesExpression] = useQueryState(
|
||||
K8S_ENTITY_TRACES_EXPRESSION_KEY,
|
||||
parseAsString,
|
||||
);
|
||||
|
||||
const handleTabChange = (value: string | null): void => {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
void setSelectedView(value);
|
||||
void setLogFiltersParam(null);
|
||||
void setTracesFiltersParam(null);
|
||||
void setEventsFiltersParam(null);
|
||||
void logEvent(InfraMonitoringEvents.TabChanged, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleExplorePagesRedirect = (): void => {
|
||||
const urlQuery = new URLSearchParams();
|
||||
|
||||
if (selectedInterval !== 'custom') {
|
||||
urlQuery.set(QueryParams.relativeTime, selectedInterval);
|
||||
} else {
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
// Explorer URL params are in milliseconds, timeRange is in seconds
|
||||
urlQuery.set(QueryParams.startTime, (timeRange.startTime * 1000).toString());
|
||||
urlQuery.set(QueryParams.endTime, (timeRange.endTime * 1000).toString());
|
||||
}
|
||||
|
||||
void logEvent(InfraMonitoringEvents.ExploreClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category: eventCategory,
|
||||
view: selectedView,
|
||||
});
|
||||
|
||||
if (selectedView === VIEW_TYPES.LOGS) {
|
||||
const fullExpression = combineInitialAndUserExpression(
|
||||
logsAndTracesInitialExpression,
|
||||
userLogsExpression || '',
|
||||
);
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.logs,
|
||||
aggregateOperator: LogsAggregatorOperator.NOOP,
|
||||
expression: fullExpression,
|
||||
filter: { expression: fullExpression },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
openInNewTab(`${ROUTES.LOGS_EXPLORER}?${urlQuery.toString()}`);
|
||||
} else if (selectedView === VIEW_TYPES.TRACES) {
|
||||
const fullExpression = combineInitialAndUserExpression(
|
||||
logsAndTracesInitialExpression,
|
||||
userTracesExpression || '',
|
||||
);
|
||||
|
||||
const compositeQuery = {
|
||||
...initialQueryState,
|
||||
queryType: 'builder',
|
||||
builder: {
|
||||
...initialQueryState.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
aggregateOperator: TracesAggregatorOperator.NOOP,
|
||||
expression: fullExpression,
|
||||
filter: { expression: fullExpression },
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
urlQuery.set('compositeQuery', JSON.stringify(compositeQuery));
|
||||
|
||||
openInNewTab(`${ROUTES.TRACES_EXPLORER}?${urlQuery.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="entity-detail-drawer__entity">
|
||||
<div className="entity-details-grid">
|
||||
<div className="labels-row">
|
||||
{metadataConfig.map((config) => (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
color="muted"
|
||||
className="entity-details-metadata-label"
|
||||
>
|
||||
{config.label}
|
||||
</Typography.Text>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="values-row">
|
||||
{metadataConfig.map((config) => {
|
||||
const value = config.getValue(entity);
|
||||
const displayValue = String(value);
|
||||
return (
|
||||
<Typography.Text
|
||||
key={config.label}
|
||||
className="entity-details-metadata-value"
|
||||
>
|
||||
{config.render ? (
|
||||
config.render(value, entity)
|
||||
) : (
|
||||
<Tooltip title={displayValue}>{displayValue}</Tooltip>
|
||||
)}
|
||||
</Typography.Text>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{countsConfig &&
|
||||
countsConfig.length > 0 &&
|
||||
selectedItem &&
|
||||
getCountsFilterExpression && (
|
||||
<EntityCountsSection
|
||||
entity={entity}
|
||||
countsConfig={countsConfig}
|
||||
selectedItem={selectedItem}
|
||||
filterExpression={getCountsFilterExpression(entity)}
|
||||
closeDrawer={handleClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!hideDetailViewTabs && (
|
||||
<div className="views-tabs-container">
|
||||
<ToggleGroupSimple
|
||||
type="single"
|
||||
className="views-tabs"
|
||||
onChange={handleTabChange}
|
||||
value={selectedView}
|
||||
items={[
|
||||
...(tabVisibility.showMetrics
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.METRICS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<BarChart size={14} />
|
||||
Metrics
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showLogs
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.LOGS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<ScrollText size={14} />
|
||||
Logs
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showTraces
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.TRACES,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<DraftingCompass size={14} />
|
||||
Traces
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(tabVisibility.showEvents
|
||||
? [
|
||||
{
|
||||
value: VIEW_TYPES.EVENTS,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
<ChevronsLeftRight size={14} />
|
||||
Events
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(customTabs?.map((tab) => ({
|
||||
value: tab.key,
|
||||
label: (
|
||||
<div className="view-title">
|
||||
{tab.icon}
|
||||
{tab.label}
|
||||
</div>
|
||||
),
|
||||
})) ?? []),
|
||||
]}
|
||||
/>
|
||||
|
||||
{selectedView === VIEW_TYPES.LOGS && (
|
||||
<Tooltip title="Go to Logs Explorer" placement="left">
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{selectedView === VIEW_TYPES.TRACES && (
|
||||
<Tooltip title="Go to Traces Explorer" placement="left">
|
||||
<Button
|
||||
icon={<Compass size={18} />}
|
||||
className="compass-button"
|
||||
onClick={handleExplorePagesRedirect}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{effectiveView === VIEW_TYPES.METRICS && (
|
||||
<EntityMetrics<T>
|
||||
entity={entity}
|
||||
eventEntity={eventCategory}
|
||||
entityWidgetInfo={entityWidgetInfo}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
category={category}
|
||||
queryKey={`${queryKeyPrefix}Metrics`}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.LOGS && (
|
||||
<EntityLogs
|
||||
eventEntity={eventCategory}
|
||||
queryKey={`${queryKeyPrefix}Logs`}
|
||||
category={category}
|
||||
initialExpression={logsAndTracesInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.TRACES && (
|
||||
<EntityTraces
|
||||
eventEntity={eventCategory}
|
||||
queryKey={`${queryKeyPrefix}Traces`}
|
||||
category={category}
|
||||
initialExpression={logsAndTracesInitialExpression}
|
||||
/>
|
||||
)}
|
||||
{effectiveView === VIEW_TYPES.EVENTS && tabVisibility.showEvents && (
|
||||
<EntityEvents
|
||||
eventEntity={eventCategory}
|
||||
queryKey={`${queryKeyPrefix}Events`}
|
||||
initialExpression={eventsInitialExpression}
|
||||
category={category}
|
||||
/>
|
||||
)}
|
||||
{customTabs?.map((tab) =>
|
||||
selectedView === tab.key ? (
|
||||
<Fragment key={tab.key}>
|
||||
{tab.render({
|
||||
entity,
|
||||
timeRange,
|
||||
selectedInterval,
|
||||
handleTimeChange,
|
||||
})}
|
||||
</Fragment>
|
||||
) : null,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -245,7 +245,6 @@ function K8sHeader<TData>({
|
||||
showAutoRefresh={showAutoRefresh}
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
defaultRelativeTime="30m"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ export function EntityCountsSection<T>({
|
||||
},
|
||||
};
|
||||
|
||||
// TODO(H4ad): After https://github.com/SigNoz/signoz/pull/12038, inherit custom time of drawer to list
|
||||
const urlParams = new URLSearchParams();
|
||||
urlParams.set(INFRA_MONITORING_K8S_PARAMS_KEYS.CATEGORY, targetCategory);
|
||||
urlParams.set(
|
||||
@@ -59,44 +60,6 @@ export function EntityCountsSection<T>({
|
||||
encodeURIComponent(JSON.stringify(compositeQuery)),
|
||||
);
|
||||
|
||||
const currentSearchParams = new URLSearchParams(window.location.search);
|
||||
const detailRelativeTime = currentSearchParams.get(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_RELATIVE_TIME,
|
||||
);
|
||||
const detailStartTime = currentSearchParams.get(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_START_TIME,
|
||||
);
|
||||
const detailEndTime = currentSearchParams.get(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_END_TIME,
|
||||
);
|
||||
|
||||
const listRelativeTime = currentSearchParams.get(QueryParams.relativeTime);
|
||||
const listStartTime = currentSearchParams.get(QueryParams.startTime);
|
||||
const listEndTime = currentSearchParams.get(QueryParams.endTime);
|
||||
|
||||
if (listRelativeTime) {
|
||||
urlParams.set(QueryParams.relativeTime, listRelativeTime);
|
||||
} else if (listStartTime && listEndTime) {
|
||||
urlParams.set(QueryParams.startTime, listStartTime);
|
||||
urlParams.set(QueryParams.endTime, listEndTime);
|
||||
}
|
||||
|
||||
if (detailRelativeTime) {
|
||||
urlParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_RELATIVE_TIME,
|
||||
detailRelativeTime,
|
||||
);
|
||||
} else if (detailStartTime && detailEndTime) {
|
||||
urlParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_START_TIME,
|
||||
detailStartTime,
|
||||
);
|
||||
urlParams.set(
|
||||
INFRA_MONITORING_K8S_PARAMS_KEYS.DETAIL_END_TIME,
|
||||
detailEndTime,
|
||||
);
|
||||
}
|
||||
|
||||
return `${ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES}?${urlParams.toString()}`;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,3 @@
|
||||
import { ReactNode } from 'react';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { EntityCountConfig } from './components/EntityCountsSection/EntityCountsSection';
|
||||
import { InfraMonitoringEntity } from '../constants';
|
||||
import { SelectedItemParams } from '../hooks';
|
||||
|
||||
export type K8sDetailsCountConfig<T> = EntityCountConfig<T>;
|
||||
|
||||
export type K8sBaseFilters = {
|
||||
filter: {
|
||||
expression: string;
|
||||
@@ -47,100 +33,3 @@ export type K8sTableRowData<T> = T & {
|
||||
/** Metadata about which attributes were used for grouping */
|
||||
groupedByMeta?: Record<string, string>;
|
||||
};
|
||||
|
||||
export interface K8sDetailsMetadataConfig<T> {
|
||||
label: string;
|
||||
getValue: (entity: T) => string | number;
|
||||
render?: (value: string | number, entity: T) => ReactNode;
|
||||
}
|
||||
|
||||
export interface K8sDetailsFilters {
|
||||
filter: { expression: string };
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
export interface K8sDetailsWidgetInfo {
|
||||
title: string;
|
||||
yAxisUnit: string;
|
||||
}
|
||||
|
||||
export type GetEntityQueryPayload<T> = (
|
||||
entity: T,
|
||||
start: number,
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
) => GetQueryResultsProps[];
|
||||
|
||||
export interface K8sDetailsTabsConfig {
|
||||
showMetrics?: boolean;
|
||||
showLogs?: boolean;
|
||||
showTraces?: boolean;
|
||||
showEvents?: boolean;
|
||||
}
|
||||
|
||||
export interface K8sDetailsCustomTabRenderProps<T> {
|
||||
entity: T;
|
||||
/** Time range in seconds — see useEntityDetailsTime */
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: Time;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface K8sDetailsCustomTab<T> {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: ReactNode;
|
||||
render: (props: K8sDetailsCustomTabRenderProps<T>) => ReactNode;
|
||||
}
|
||||
|
||||
export interface K8sBaseDetailsProps<T> {
|
||||
category: InfraMonitoringEntity;
|
||||
eventCategory: string;
|
||||
// Data fetching configuration
|
||||
getSelectedItemExpression: (params: SelectedItemParams) => string;
|
||||
fetchEntityData: (
|
||||
filters: K8sDetailsFilters,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<{ data: T | null; error?: APIError | null }>;
|
||||
// Entity configuration
|
||||
getEntityName: (entity: T) => string;
|
||||
getInitialLogTracesExpression: (entity: T) => string;
|
||||
getInitialEventsExpression: (entity: T) => string;
|
||||
metadataConfig: K8sDetailsMetadataConfig<T>[];
|
||||
countsConfig?: K8sDetailsCountConfig<T>[];
|
||||
getCountsFilterExpression?: (entity: T) => string;
|
||||
entityWidgetInfo: K8sDetailsWidgetInfo[];
|
||||
getEntityQueryPayload: GetEntityQueryPayload<T>;
|
||||
queryKeyPrefix: string;
|
||||
/** When true, only metrics are shown and the Metrics/Logs/Traces/Events tab bar is hidden. */
|
||||
hideDetailViewTabs?: boolean;
|
||||
tabsConfig?: K8sDetailsTabsConfig;
|
||||
customTabs?: K8sDetailsCustomTab<T>[];
|
||||
}
|
||||
|
||||
export interface K8sBaseDetailsContentProps<T> {
|
||||
entity: T;
|
||||
category: InfraMonitoringEntity;
|
||||
eventCategory: string;
|
||||
metadataConfig: K8sDetailsMetadataConfig<T>[];
|
||||
countsConfig?: K8sDetailsCountConfig<T>[];
|
||||
getCountsFilterExpression?: (entity: T) => string;
|
||||
selectedItem: string | null;
|
||||
handleClose: () => void;
|
||||
entityWidgetInfo: K8sDetailsWidgetInfo[];
|
||||
getEntityQueryPayload: GetEntityQueryPayload<T>;
|
||||
queryKeyPrefix: string;
|
||||
hideDetailViewTabs: boolean;
|
||||
tabsConfig?: K8sDetailsTabsConfig;
|
||||
customTabs?: K8sDetailsCustomTab<T>[];
|
||||
logsAndTracesInitialExpression: string;
|
||||
eventsInitialExpression: string;
|
||||
}
|
||||
|
||||
// Aliases for backward compatibility
|
||||
export type CustomTab<T> = K8sDetailsCustomTab<T>;
|
||||
export type CustomTabRenderProps<T> = K8sDetailsCustomTabRenderProps<T>;
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import { Undo } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
|
||||
import { useEntityDetailsTime } from './useEntityDetailsTime';
|
||||
|
||||
interface EntityDateTimeSelectorProps {
|
||||
eventEntity: string;
|
||||
category: InfraMonitoringEntity;
|
||||
view: string;
|
||||
}
|
||||
|
||||
function EntityDateTimeSelector({
|
||||
eventEntity,
|
||||
category,
|
||||
view,
|
||||
}: EntityDateTimeSelectorProps): JSX.Element {
|
||||
const {
|
||||
timeRange,
|
||||
selectedInterval,
|
||||
handleTimeChange,
|
||||
handleResetToParentTime,
|
||||
hasTimeChanged,
|
||||
} = useEntityDetailsTime();
|
||||
|
||||
const onTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
void logEvent(InfraMonitoringEvents.TimeUpdated, {
|
||||
entity: eventEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
view,
|
||||
interval,
|
||||
});
|
||||
handleTimeChange(interval, dateTimeRange);
|
||||
},
|
||||
[category, view, eventEntity, handleTimeChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="entity-date-time-selector">
|
||||
{hasTimeChanged && (
|
||||
<TooltipSimple title="Reset to list time" side="bottom">
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={handleResetToParentTime}
|
||||
data-testid="reset-to-list-time-button"
|
||||
prefix={<Undo size={14} />}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</TooltipSimple>
|
||||
)}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection
|
||||
onTimeChange={onTimeChange}
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default EntityDateTimeSelector;
|
||||
@@ -1,95 +0,0 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
isCustomTimeRange,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
useGlobalTime,
|
||||
} from 'store/globalTime';
|
||||
|
||||
export interface EntityDetailsTimeRange {
|
||||
/** Unix timestamp in seconds — the unit API payload builders expect. Multiply by 1000 for ms-based UI (modals, URL params). */
|
||||
startTime: number;
|
||||
/** Unix timestamp in seconds — the unit API payload builders expect. Multiply by 1000 for ms-based UI (modals, URL params). */
|
||||
endTime: number;
|
||||
}
|
||||
|
||||
export interface UseEntityDetailsTimeResult {
|
||||
/** Time range in seconds */
|
||||
timeRange: EntityDetailsTimeRange;
|
||||
selectedInterval: Time;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
handleResetToParentTime: () => boolean;
|
||||
canResetToParent: boolean;
|
||||
hasTimeChanged: boolean;
|
||||
}
|
||||
|
||||
export function useEntityDetailsTime(): UseEntityDetailsTimeResult {
|
||||
const selectedTime = useGlobalTime((s) => s.selectedTime);
|
||||
const lastComputedMinMax = useGlobalTime((s) => s.lastComputedMinMax);
|
||||
const setSelectedTime = useGlobalTime((s) => s.setSelectedTime);
|
||||
const handleResetToParentTime = useGlobalTime((s) => s.resetToParentTime);
|
||||
const parentStore = useGlobalTime((s) => s.parentStore);
|
||||
|
||||
const canResetToParent = !!parentStore;
|
||||
|
||||
const hasTimeChanged = useMemo(() => {
|
||||
if (!parentStore) {
|
||||
return false;
|
||||
}
|
||||
return selectedTime !== parentStore.getState().selectedTime;
|
||||
}, [parentStore, selectedTime]);
|
||||
|
||||
const timeRange = useMemo<EntityDetailsTimeRange>(
|
||||
() => ({
|
||||
startTime: Math.floor(
|
||||
lastComputedMinMax.minTime / NANO_SECOND_MULTIPLIER / 1000,
|
||||
),
|
||||
endTime: Math.floor(
|
||||
lastComputedMinMax.maxTime / NANO_SECOND_MULTIPLIER / 1000,
|
||||
),
|
||||
}),
|
||||
[lastComputedMinMax],
|
||||
);
|
||||
|
||||
const selectedInterval = useMemo<Time>(
|
||||
() =>
|
||||
isCustomTimeRange(selectedTime)
|
||||
? ('custom' as Time)
|
||||
: (selectedTime as Time),
|
||||
[selectedTime],
|
||||
);
|
||||
|
||||
const handleTimeChange = useCallback(
|
||||
(interval: Time | CustomTimeType, dateTimeRange?: [number, number]): void => {
|
||||
if (interval === 'custom' && dateTimeRange) {
|
||||
// DateTimeSelector delivers milliseconds; the store keys custom
|
||||
// ranges by nanoseconds.
|
||||
setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
dateTimeRange[0] * NANO_SECOND_MULTIPLIER,
|
||||
dateTimeRange[1] * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setSelectedTime(interval as Time);
|
||||
}
|
||||
},
|
||||
[setSelectedTime],
|
||||
);
|
||||
|
||||
return {
|
||||
timeRange,
|
||||
selectedInterval,
|
||||
handleTimeChange,
|
||||
handleResetToParentTime,
|
||||
canResetToParent,
|
||||
hasTimeChanged,
|
||||
};
|
||||
}
|
||||
@@ -21,14 +21,17 @@ import Controls from 'container/Controls';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import LoadingContainer from 'container/InfraMonitoringK8sV2/LoadingContainer';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
|
||||
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
|
||||
import EntityError from '../EntityError/EntityError';
|
||||
import { EventContents } from './EventsContent';
|
||||
@@ -50,7 +53,16 @@ interface EventDataType {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
eventEntity: string;
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
queryKey: string;
|
||||
category: InfraMonitoringEntity;
|
||||
initialExpression: string;
|
||||
@@ -65,11 +77,13 @@ const handleExpandRow = (record: EventDataType): JSX.Element => (
|
||||
);
|
||||
|
||||
function EntityEventsContent({
|
||||
eventEntity,
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
selectedInterval,
|
||||
queryKey,
|
||||
category,
|
||||
}: Omit<Props, 'initialExpression'>): JSX.Element {
|
||||
const { timeRange } = useEntityDetailsTime();
|
||||
const expression = useExpression();
|
||||
const inputExpression = useInputExpression();
|
||||
const userExpression = useUserExpression();
|
||||
@@ -117,8 +131,8 @@ function EntityEventsContent({
|
||||
if (validation.isValid) {
|
||||
querySearchOnRun(newUserExpression || '');
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: eventEntity,
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
view: InfraMonitoringEvents.EventsView,
|
||||
@@ -127,14 +141,7 @@ function EntityEventsContent({
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
[
|
||||
inputExpression,
|
||||
initialExpression,
|
||||
refetch,
|
||||
querySearchOnRun,
|
||||
category,
|
||||
eventEntity,
|
||||
],
|
||||
[inputExpression, initialExpression, refetch, querySearchOnRun, category],
|
||||
);
|
||||
|
||||
const queryData = useMemo(
|
||||
@@ -222,10 +229,16 @@ function EntityEventsContent({
|
||||
<div className={styles.container}>
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterContainerTime}>
|
||||
<EntityDateTimeSelector
|
||||
eventEntity={eventEntity}
|
||||
category={category}
|
||||
view={InfraMonitoringEvents.EventsView}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
|
||||
<RunQueryBtn
|
||||
|
||||
@@ -29,25 +29,25 @@ function verifyEntityEventsV5Request({
|
||||
expect(orderKeys).toContain('timestamp');
|
||||
}
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
default: ({
|
||||
onTimeChange,
|
||||
}: {
|
||||
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
|
||||
}): JSX.Element => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="mock-datetime-selection"
|
||||
onClick={(): void => {
|
||||
onTimeChange?.('5m');
|
||||
}}
|
||||
>
|
||||
Select Time
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
useEntityDetailsTime: (): {
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: string;
|
||||
handleTimeChange: jest.Mock;
|
||||
} => ({
|
||||
timeRange: { startTime: 1, endTime: 2 },
|
||||
selectedInterval: '5m',
|
||||
handleTimeChange: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('EntityEvents', () => {
|
||||
let capturedQueryRangePayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
@@ -69,7 +69,10 @@ describe('EntityEvents', () => {
|
||||
searchParams={`${K8S_ENTITY_EVENTS_EXPRESSION_KEY}=k8s.pod.name+%3D+%22x%22`}
|
||||
>
|
||||
<EntityEvents
|
||||
eventEntity="test"
|
||||
timeRange={{ startTime: 1, endTime: 2 }}
|
||||
isModalTimeSelection={false}
|
||||
handleTimeChange={jest.fn()}
|
||||
selectedInterval="5m"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression='k8s.pod.name = "x"'
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
|
||||
import { mockQueryRangeV5WithEventsResponse } from '__tests__/query_range_v5.util';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
createGlobalTimeStore,
|
||||
GlobalTimeContext,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime';
|
||||
import { act, render, waitFor } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
import EntityEvents from '../EntityEvents';
|
||||
import { K8S_ENTITY_EVENTS_EXPRESSION_KEY } from '../hooks';
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
|
||||
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
|
||||
|
||||
describe('EntityEvents time range wiring', () => {
|
||||
let capturedPayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
capturedPayloads = [];
|
||||
mockQueryRangeV5WithEventsResponse({
|
||||
onReceiveRequest: async (req) => {
|
||||
const body = (await req.json()) as QueryRangePayloadV5;
|
||||
capturedPayloads.push(body);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
mockFieldsAPIsWithEmptyResponse();
|
||||
});
|
||||
|
||||
it('should query the V5 API with the global time range converted to milliseconds', async () => {
|
||||
const store = createGlobalTimeStore();
|
||||
store
|
||||
.getState()
|
||||
.setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
START_MS * NANO_SECOND_MULTIPLIER,
|
||||
END_MS * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
|
||||
const expression = 'k8s.pod.name = "test-pod"';
|
||||
|
||||
act(() => {
|
||||
render(
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={`${K8S_ENTITY_EVENTS_EXPRESSION_KEY}=${encodeURIComponent(
|
||||
expression,
|
||||
)}`}
|
||||
>
|
||||
<EntityEvents
|
||||
eventEntity="test"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression={expression}
|
||||
/>
|
||||
</NuqsTestingAdapter>
|
||||
</GlobalTimeContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads).toHaveLength(1);
|
||||
});
|
||||
|
||||
expect(capturedPayloads[0].start).toBe(START_MS);
|
||||
expect(capturedPayloads[0].end).toBe(END_MS);
|
||||
});
|
||||
});
|
||||
@@ -25,6 +25,11 @@ import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants'
|
||||
import { LogsLoading } from 'container/LogsLoading/LogsLoading';
|
||||
import { FontSize } from 'container/OptionsMenu/types';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
|
||||
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
|
||||
import useScrollToLog from 'hooks/logs/useScrollToLog';
|
||||
@@ -33,8 +38,6 @@ import { ILog } from 'types/api/logs/log';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
|
||||
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
|
||||
import EntityError from '../EntityError/EntityError';
|
||||
import { isKeyNotFoundError } from '../utils';
|
||||
@@ -44,18 +47,29 @@ import { getEntityLogsQueryPayload } from './utils';
|
||||
import styles from './EntityLogs.module.scss';
|
||||
|
||||
interface Props {
|
||||
eventEntity: string;
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
queryKey: string;
|
||||
category: InfraMonitoringEntity;
|
||||
initialExpression: string;
|
||||
}
|
||||
|
||||
function EntityLogsContent({
|
||||
eventEntity,
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
selectedInterval,
|
||||
queryKey,
|
||||
category,
|
||||
}: Omit<Props, 'initialExpression'>): JSX.Element {
|
||||
const { timeRange } = useEntityDetailsTime();
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null);
|
||||
|
||||
const expression = useExpression();
|
||||
@@ -120,7 +134,7 @@ function EntityLogsContent({
|
||||
if (validation.isValid) {
|
||||
querySearchOnRun(newUserExpression);
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
@@ -225,10 +239,16 @@ function EntityLogsContent({
|
||||
<div className={styles.container}>
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterContainerTime}>
|
||||
<EntityDateTimeSelector
|
||||
eventEntity={eventEntity}
|
||||
category={category}
|
||||
view={InfraMonitoringEvents.LogsView}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
|
||||
<RunQueryBtn
|
||||
|
||||
@@ -50,25 +50,25 @@ jest.mock(
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
default: ({
|
||||
onTimeChange,
|
||||
}: {
|
||||
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
|
||||
}): JSX.Element => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="mock-datetime-selection"
|
||||
onClick={(): void => {
|
||||
onTimeChange?.('5m');
|
||||
}}
|
||||
>
|
||||
Select Time
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
useEntityDetailsTime: (): {
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: string;
|
||||
handleTimeChange: jest.Mock;
|
||||
} => ({
|
||||
timeRange: { startTime: 1, endTime: 2 },
|
||||
selectedInterval: '5m',
|
||||
handleTimeChange: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('EntityLogs', () => {
|
||||
let capturedQueryRangePayloads: QueryRangePayloadV5[] = [];
|
||||
const itemHeight = 100;
|
||||
@@ -94,7 +94,10 @@ describe('EntityLogs', () => {
|
||||
>
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 500, itemHeight }}>
|
||||
<EntityLogs
|
||||
eventEntity="test"
|
||||
timeRange={{ startTime: 1, endTime: 2 }}
|
||||
isModalTimeSelection={false}
|
||||
handleTimeChange={jest.fn()}
|
||||
selectedInterval="5m"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression='k8s.pod.name = "x"'
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
|
||||
import { mockQueryRangeV5WithLogsResponse } from '__tests__/query_range_v5.util';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
createGlobalTimeStore,
|
||||
GlobalTimeContext,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime';
|
||||
import { act, render, waitFor } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
import EntityLogs from '../EntityLogs';
|
||||
import { K8S_ENTITY_LOGS_EXPRESSION_KEY } from '../hooks';
|
||||
|
||||
jest.mock(
|
||||
'components/OverlayScrollbar/OverlayScrollbar',
|
||||
() =>
|
||||
function MockOverlayScrollbar({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}): JSX.Element {
|
||||
return <div>{children}</div>;
|
||||
},
|
||||
);
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
|
||||
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
|
||||
|
||||
describe('EntityLogs time range wiring', () => {
|
||||
let capturedPayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
capturedPayloads = [];
|
||||
mockQueryRangeV5WithLogsResponse({
|
||||
onReceiveRequest: async (req) => {
|
||||
const body = (await req.json()) as QueryRangePayloadV5;
|
||||
capturedPayloads.push(body);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
mockFieldsAPIsWithEmptyResponse();
|
||||
});
|
||||
|
||||
it('should query the V5 API with the global time range converted to milliseconds', async () => {
|
||||
const store = createGlobalTimeStore();
|
||||
store
|
||||
.getState()
|
||||
.setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
START_MS * NANO_SECOND_MULTIPLIER,
|
||||
END_MS * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
|
||||
const expression = 'k8s.pod.name = "test-pod"';
|
||||
|
||||
act(() => {
|
||||
render(
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={`${K8S_ENTITY_LOGS_EXPRESSION_KEY}=${encodeURIComponent(
|
||||
expression,
|
||||
)}`}
|
||||
>
|
||||
<EntityLogs
|
||||
eventEntity="test"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression={expression}
|
||||
/>
|
||||
</NuqsTestingAdapter>
|
||||
</GlobalTimeContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
expect(capturedPayloads[0].start).toBe(START_MS);
|
||||
expect(capturedPayloads[0].end).toBe(END_MS);
|
||||
});
|
||||
});
|
||||
@@ -2,11 +2,15 @@ import { useCallback, useMemo, useRef } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
import { Skeleton } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import TimeSeries from 'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries';
|
||||
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { useResizeObserver } from 'hooks/useDimensions';
|
||||
@@ -20,17 +24,25 @@ import { getMetricsExplorerUrl } from 'utils/explorerUtils';
|
||||
import { buildEntityMetricsChartConfig } from './configBuilder';
|
||||
import ChartHeader from './ChartHeader';
|
||||
|
||||
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
|
||||
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import { useEntityMetrics } from './hooks';
|
||||
import { isKeyNotFoundError } from '../utils';
|
||||
|
||||
import styles from './EntityMetrics.module.scss';
|
||||
import { MetricsTable } from './MetricsTable';
|
||||
import { DEFAULT_TIME_RANGE } from 'container/TopNav/DateTimeSelectionV2/constants';
|
||||
|
||||
interface EntityMetricsProps<T> {
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
entity: T;
|
||||
eventEntity: string;
|
||||
entityWidgetInfo: {
|
||||
title: string;
|
||||
yAxisUnit: string;
|
||||
@@ -47,16 +59,16 @@ interface EntityMetricsProps<T> {
|
||||
}
|
||||
|
||||
function EntityMetrics<T>({
|
||||
selectedInterval,
|
||||
entity,
|
||||
eventEntity,
|
||||
timeRange,
|
||||
handleTimeChange,
|
||||
isModalTimeSelection,
|
||||
entityWidgetInfo,
|
||||
getEntityQueryPayload,
|
||||
queryKey,
|
||||
category,
|
||||
}: EntityMetricsProps<T>): JSX.Element {
|
||||
const { timeRange, selectedInterval, handleTimeChange } =
|
||||
useEntityDetailsTime();
|
||||
|
||||
const { visibilities, setElement } = useMultiIntersectionObserver(
|
||||
entityWidgetInfo.length,
|
||||
{ threshold: 0.1 },
|
||||
@@ -79,8 +91,10 @@ function EntityMetrics<T>({
|
||||
|
||||
const onDragSelect = useCallback(
|
||||
(start: number, end: number): void => {
|
||||
// UPlotConfigBuilder's setSelect hook already delivers milliseconds
|
||||
handleTimeChange('custom', [Math.trunc(start), Math.trunc(end)]);
|
||||
const startTimestamp = Math.trunc(start);
|
||||
const endTimestamp = Math.trunc(end);
|
||||
|
||||
handleTimeChange('custom', [startTimestamp, endTimestamp]);
|
||||
},
|
||||
[handleTimeChange],
|
||||
);
|
||||
@@ -174,10 +188,16 @@ function EntityMetrics<T>({
|
||||
return (
|
||||
<>
|
||||
<div className={styles.metricsHeader}>
|
||||
<EntityDateTimeSelector
|
||||
eventEntity={eventEntity}
|
||||
category={category}
|
||||
view={InfraMonitoringEvents.MetricsView}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime={DEFAULT_TIME_RANGE}
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.entityMetricsContainer}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { Time } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import * as appContextHooks from 'providers/App/App';
|
||||
import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
import uPlot from 'uplot';
|
||||
@@ -27,28 +28,13 @@ jest.mock('lib/uPlotV2/utils/dataUtils', () => ({
|
||||
hasSingleVisiblePoint: jest.fn().mockReturnValue(false),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="date-time-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockTimeRange = { startTime: 1705315200, endTime: 1705318800 };
|
||||
let mockSelectedInterval = '5m';
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
useEntityDetailsTime: (): {
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: string;
|
||||
handleTimeChange: jest.Mock;
|
||||
} => ({
|
||||
timeRange: mockTimeRange,
|
||||
selectedInterval: mockSelectedInterval,
|
||||
handleTimeChange: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock(
|
||||
'container/DashboardContainer/visualization/charts/TimeSeries/TimeSeries',
|
||||
() => ({
|
||||
@@ -158,6 +144,13 @@ const mockGetEntityQueryPayload = jest.fn().mockReturnValue([
|
||||
},
|
||||
]);
|
||||
|
||||
const mockTimeRange = {
|
||||
startTime: 1705315200,
|
||||
endTime: 1705318800,
|
||||
};
|
||||
|
||||
const mockHandleTimeChange = jest.fn();
|
||||
|
||||
const mockQueries = [
|
||||
{
|
||||
data: {
|
||||
@@ -290,6 +283,10 @@ const mockEmptyQueries = [
|
||||
|
||||
const renderEntityMetrics = (overrides = {}): any => {
|
||||
const defaultProps = {
|
||||
timeRange: mockTimeRange,
|
||||
isModalTimeSelection: false,
|
||||
handleTimeChange: mockHandleTimeChange,
|
||||
selectedInterval: '5m' as Time,
|
||||
entity: mockEntity,
|
||||
entityWidgetInfo: mockEntityWidgetInfo,
|
||||
getEntityQueryPayload: mockGetEntityQueryPayload,
|
||||
@@ -301,8 +298,11 @@ const renderEntityMetrics = (overrides = {}): any => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<EntityMetrics
|
||||
timeRange={defaultProps.timeRange}
|
||||
isModalTimeSelection={defaultProps.isModalTimeSelection}
|
||||
handleTimeChange={defaultProps.handleTimeChange}
|
||||
selectedInterval={defaultProps.selectedInterval}
|
||||
entity={defaultProps.entity}
|
||||
eventEntity="test"
|
||||
entityWidgetInfo={defaultProps.entityWidgetInfo}
|
||||
getEntityQueryPayload={defaultProps.getEntityQueryPayload}
|
||||
queryKey={defaultProps.queryKey}
|
||||
@@ -344,7 +344,6 @@ const mockQueryPayloads = [
|
||||
describe('EntityMetrics', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSelectedInterval = '5m';
|
||||
mockUseEntityMetrics.mockReturnValue({
|
||||
queries: mockQueries as any,
|
||||
chartData: mockChartData,
|
||||
@@ -455,8 +454,7 @@ describe('EntityMetrics', () => {
|
||||
});
|
||||
|
||||
it('builds metrics explorer link with relativeTime when a relative interval is selected', () => {
|
||||
mockSelectedInterval = '5m';
|
||||
renderEntityMetrics();
|
||||
renderEntityMetrics({ selectedInterval: '5m' as Time });
|
||||
const href = screen
|
||||
.getByTestId('open-metrics-explorer-0')
|
||||
.getAttribute('href');
|
||||
@@ -466,8 +464,7 @@ describe('EntityMetrics', () => {
|
||||
});
|
||||
|
||||
it('builds metrics explorer link with absolute time range in milliseconds for custom interval', () => {
|
||||
mockSelectedInterval = 'custom';
|
||||
renderEntityMetrics();
|
||||
renderEntityMetrics({ selectedInterval: 'custom' as Time });
|
||||
const href = screen
|
||||
.getByTestId('open-metrics-explorer-0')
|
||||
.getAttribute('href');
|
||||
@@ -481,6 +478,7 @@ describe('EntityMetrics', () => {
|
||||
expect(mockUseEntityMetrics).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
queryKey: 'test-query-key',
|
||||
timeRange: mockTimeRange,
|
||||
entity: mockEntity,
|
||||
category: InfraMonitoringEntity.PODS,
|
||||
}),
|
||||
|
||||
@@ -1,119 +0,0 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
createGlobalTimeStore,
|
||||
GlobalTimeContext,
|
||||
GlobalTimeStoreApi,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime';
|
||||
import { act, render } from 'tests/test-utils';
|
||||
|
||||
import { buildEntityMetricsChartConfig } from '../configBuilder';
|
||||
import EntityMetrics from '../EntityMetrics';
|
||||
|
||||
jest.mock('../configBuilder', () => ({
|
||||
buildEntityMetricsChartConfig: jest.fn().mockReturnValue({
|
||||
getId: jest.fn().mockReturnValue('mock-id'),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="date-time-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockBuildChartConfig = buildEntityMetricsChartConfig as jest.Mock;
|
||||
|
||||
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
|
||||
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
|
||||
const START_SECONDS = 1705315200;
|
||||
const END_SECONDS = 1705318800;
|
||||
|
||||
const entity = { id: 'test-entity-1' };
|
||||
|
||||
// The jsdom IntersectionObserver polyfill never reports visibility, so the
|
||||
// queries stay disabled and no network request is issued.
|
||||
const queryPayload = {
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
} as unknown as GetQueryResultsProps;
|
||||
|
||||
function createStoreWithCustomRange(): GlobalTimeStoreApi {
|
||||
const store = createGlobalTimeStore();
|
||||
store
|
||||
.getState()
|
||||
.setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
START_MS * NANO_SECOND_MULTIPLIER,
|
||||
END_MS * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
return store;
|
||||
}
|
||||
|
||||
function renderEntityMetrics(store: GlobalTimeStoreApi): jest.Mock {
|
||||
const getEntityQueryPayload = jest.fn().mockReturnValue([queryPayload]);
|
||||
render(
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
<EntityMetrics
|
||||
entity={entity}
|
||||
eventEntity="test"
|
||||
entityWidgetInfo={[{ title: 'CPU Usage', yAxisUnit: 'percentage' }]}
|
||||
getEntityQueryPayload={getEntityQueryPayload}
|
||||
queryKey="test-query-key"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
/>
|
||||
</GlobalTimeContext.Provider>,
|
||||
);
|
||||
return getEntityQueryPayload;
|
||||
}
|
||||
|
||||
describe('EntityMetrics time range wiring', () => {
|
||||
beforeEach(() => {
|
||||
mockBuildChartConfig.mockClear();
|
||||
});
|
||||
|
||||
it('should build the metrics query payloads and chart config from the global time range in seconds', () => {
|
||||
const store = createStoreWithCustomRange();
|
||||
|
||||
const getEntityQueryPayload = renderEntityMetrics(store);
|
||||
|
||||
expect(getEntityQueryPayload).toHaveBeenCalledWith(
|
||||
entity,
|
||||
START_SECONDS,
|
||||
END_SECONDS,
|
||||
false,
|
||||
);
|
||||
expect(mockBuildChartConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
minTimeScale: START_SECONDS,
|
||||
maxTimeScale: END_SECONDS,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should store a drag selection (received in milliseconds) as the equivalent custom range', () => {
|
||||
const store = createStoreWithCustomRange();
|
||||
|
||||
renderEntityMetrics(store);
|
||||
|
||||
const { onDragSelect } = mockBuildChartConfig.mock.calls[0][0];
|
||||
|
||||
// UPlotConfigBuilder's setSelect hook calls onDragSelect with milliseconds
|
||||
const dragStartMs = 1705316000000;
|
||||
const dragEndMs = 1705317000000;
|
||||
act(() => {
|
||||
onDragSelect(dragStartMs, dragEndMs);
|
||||
});
|
||||
|
||||
expect(store.getState().selectedTime).toBe(
|
||||
createCustomTimeRange(
|
||||
dragStartMs * NANO_SECOND_MULTIPLIER,
|
||||
dragEndMs * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,11 @@ import { InfraMonitoringEvents } from 'constants/events';
|
||||
import Controls from 'container/Controls';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
|
||||
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
|
||||
import {
|
||||
CustomTimeType,
|
||||
Time,
|
||||
} from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
|
||||
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
|
||||
import { useQueryState } from 'nuqs';
|
||||
@@ -27,11 +32,10 @@ import { DataSource } from 'types/common/queryBuilder';
|
||||
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
|
||||
import { validateQuery } from 'utils/queryValidationUtils';
|
||||
|
||||
import EntityDateTimeSelector from '../EntityDateTimeSelector/EntityDateTimeSelector';
|
||||
import { useEntityDetailsTime } from '../EntityDateTimeSelector/useEntityDetailsTime';
|
||||
import EntityEmptyState from '../EntityEmptyState/EntityEmptyState';
|
||||
import EntityError from '../EntityError/EntityError';
|
||||
import { isKeyNotFoundError, selectedEntityTracesColumns } from '../utils';
|
||||
import { selectedEntityTracesColumns } from '../utils';
|
||||
import { isKeyNotFoundError } from '../utils';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY, useEntityTraces } from './hooks';
|
||||
import { getTraceListColumns } from './traceListColumns';
|
||||
import { getEntityTracesQueryPayload } from './utils';
|
||||
@@ -40,18 +44,29 @@ import styles from './EntityTraces.module.scss';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
|
||||
interface Props {
|
||||
eventEntity: string;
|
||||
timeRange: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
};
|
||||
isModalTimeSelection: boolean;
|
||||
handleTimeChange: (
|
||||
interval: Time | CustomTimeType,
|
||||
dateTimeRange?: [number, number],
|
||||
) => void;
|
||||
selectedInterval: Time;
|
||||
queryKey: string;
|
||||
category: InfraMonitoringEntity;
|
||||
initialExpression: string;
|
||||
}
|
||||
|
||||
function EntityTracesContent({
|
||||
eventEntity,
|
||||
timeRange,
|
||||
isModalTimeSelection,
|
||||
handleTimeChange,
|
||||
selectedInterval,
|
||||
queryKey,
|
||||
category,
|
||||
}: Omit<Props, 'initialExpression'>): JSX.Element {
|
||||
const { timeRange } = useEntityDetailsTime();
|
||||
const expression = useExpression();
|
||||
const inputExpression = useInputExpression();
|
||||
const userExpression = useUserExpression();
|
||||
@@ -99,8 +114,8 @@ function EntityTracesContent({
|
||||
if (validation.isValid) {
|
||||
querySearchOnRun(newUserExpression || '');
|
||||
|
||||
void logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: eventEntity,
|
||||
logEvent(InfraMonitoringEvents.FilterApplied, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
page: InfraMonitoringEvents.DetailedPage,
|
||||
category,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
@@ -109,14 +124,7 @@ function EntityTracesContent({
|
||||
refetch();
|
||||
}
|
||||
},
|
||||
[
|
||||
inputExpression,
|
||||
initialExpression,
|
||||
refetch,
|
||||
querySearchOnRun,
|
||||
category,
|
||||
eventEntity,
|
||||
],
|
||||
[inputExpression, initialExpression, refetch, querySearchOnRun, category],
|
||||
);
|
||||
|
||||
const queryData = useMemo(
|
||||
@@ -144,7 +152,7 @@ function EntityTracesContent({
|
||||
const hasAdditionalFilters = !!userExpression?.trim();
|
||||
|
||||
const handleRowClick = useCallback(() => {
|
||||
void logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
logEvent(InfraMonitoringEvents.ItemClicked, {
|
||||
entity: InfraMonitoringEvents.K8sEntity,
|
||||
category,
|
||||
view: InfraMonitoringEvents.TracesView,
|
||||
@@ -161,10 +169,16 @@ function EntityTracesContent({
|
||||
<div className={styles.container}>
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterContainerTime}>
|
||||
<EntityDateTimeSelector
|
||||
eventEntity={eventEntity}
|
||||
category={category}
|
||||
view={InfraMonitoringEvents.TracesView}
|
||||
<DateTimeSelectionV2
|
||||
showAutoRefresh
|
||||
showRefreshText={false}
|
||||
hideShareModal
|
||||
isModalTimeSelection={isModalTimeSelection}
|
||||
onTimeChange={handleTimeChange}
|
||||
defaultRelativeTime="5m"
|
||||
modalSelectedInterval={selectedInterval}
|
||||
modalInitialStartTime={timeRange.startTime * 1000}
|
||||
modalInitialEndTime={timeRange.endTime * 1000}
|
||||
/>
|
||||
|
||||
<RunQueryBtn
|
||||
|
||||
@@ -34,8 +34,10 @@ describe('EntityTraces - Default Behavior', () => {
|
||||
});
|
||||
|
||||
it('should pass time range to API (converted to milliseconds)', async () => {
|
||||
const timeRange = { startTime: 1000, endTime: 2000 };
|
||||
|
||||
act(() => {
|
||||
renderEntityTraces();
|
||||
renderEntityTraces({ timeRange });
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -45,8 +47,8 @@ describe('EntityTraces - Default Behavior', () => {
|
||||
verifyQueryPayload({
|
||||
payload: capturedPayloads[0],
|
||||
expectedTimeRange: {
|
||||
start: 1 * 1000,
|
||||
end: 2 * 1000,
|
||||
start: timeRange.startTime * 1000,
|
||||
end: timeRange.endTime * 1000,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
|
||||
import { mockQueryRangeV5WithTracesResponse } from '__tests__/query_range_v5.util';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import {
|
||||
createCustomTimeRange,
|
||||
createGlobalTimeStore,
|
||||
GlobalTimeContext,
|
||||
NANO_SECOND_MULTIPLIER,
|
||||
} from 'store/globalTime';
|
||||
import { act, render, waitFor } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
|
||||
import EntityTraces from '../EntityTraces';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../hooks';
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const START_MS = 1705315200000; // 2024-01-15T10:40:00Z
|
||||
const END_MS = 1705318800000; // 2024-01-15T11:40:00Z
|
||||
|
||||
describe('EntityTraces time range wiring', () => {
|
||||
let capturedPayloads: QueryRangePayloadV5[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
capturedPayloads = [];
|
||||
mockQueryRangeV5WithTracesResponse({
|
||||
onReceiveRequest: async (req) => {
|
||||
const body = (await req.json()) as QueryRangePayloadV5;
|
||||
capturedPayloads.push(body);
|
||||
return {};
|
||||
},
|
||||
});
|
||||
mockFieldsAPIsWithEmptyResponse();
|
||||
});
|
||||
|
||||
it('should query the V5 API with the global time range converted to milliseconds', async () => {
|
||||
const store = createGlobalTimeStore();
|
||||
store
|
||||
.getState()
|
||||
.setSelectedTime(
|
||||
createCustomTimeRange(
|
||||
START_MS * NANO_SECOND_MULTIPLIER,
|
||||
END_MS * NANO_SECOND_MULTIPLIER,
|
||||
),
|
||||
);
|
||||
|
||||
const expression = 'k8s.pod.name = "test-pod"';
|
||||
|
||||
act(() => {
|
||||
render(
|
||||
<GlobalTimeContext.Provider value={store}>
|
||||
<NuqsTestingAdapter
|
||||
searchParams={`${K8S_ENTITY_TRACES_EXPRESSION_KEY}=${encodeURIComponent(
|
||||
expression,
|
||||
)}`}
|
||||
>
|
||||
<EntityTraces
|
||||
eventEntity="test"
|
||||
queryKey="test"
|
||||
category={InfraMonitoringEntity.PODS}
|
||||
initialExpression={expression}
|
||||
/>
|
||||
</NuqsTestingAdapter>
|
||||
</GlobalTimeContext.Provider>,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(capturedPayloads).toHaveLength(1);
|
||||
});
|
||||
|
||||
expect(capturedPayloads[0].start).toBe(START_MS);
|
||||
expect(capturedPayloads[0].end).toBe(END_MS);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import { mockFieldsAPIsWithEmptyResponse } from '__tests__/fields_api.util';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
|
||||
import { render, RenderResult } from 'tests/test-utils';
|
||||
import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
@@ -7,19 +9,39 @@ import { QueryRangePayloadV5 } from 'types/api/v5/queryRange';
|
||||
import EntityTraces from '../EntityTraces';
|
||||
import { K8S_ENTITY_TRACES_EXPRESSION_KEY } from '../hooks';
|
||||
|
||||
// QuerySearch fires autocomplete requests on mount; without handlers MSW
|
||||
// passes them through to the real network and the resulting AxiosError fails
|
||||
// whichever test happens to be running.
|
||||
beforeEach(() => {
|
||||
mockFieldsAPIsWithEmptyResponse();
|
||||
server.use(
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/keys`, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: { keys: {}, complete: true } }),
|
||||
),
|
||||
),
|
||||
rest.get(`${ENVIRONMENT.baseURL}/api/v1/fields/values`, (_, res, ctx) =>
|
||||
res(
|
||||
ctx.status(200),
|
||||
ctx.json({ status: 'success', data: { values: {}, complete: true } }),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
export interface RenderEntityTracesOptions {
|
||||
expression?: string;
|
||||
timeRange?: { startTime: number; endTime: number };
|
||||
category?: InfraMonitoringEntity;
|
||||
selectedInterval?: '5m' | '15m' | '30m' | '1h';
|
||||
pagination?: { offset: number; limit: number };
|
||||
}
|
||||
|
||||
export function renderEntityTraces({
|
||||
expression = 'k8s.pod.name = "test-pod"',
|
||||
timeRange = { startTime: 1, endTime: 2 },
|
||||
category = InfraMonitoringEntity.PODS,
|
||||
selectedInterval = '5m',
|
||||
pagination,
|
||||
}: RenderEntityTracesOptions = {}): RenderResult {
|
||||
const encodedExpression = encodeURIComponent(expression);
|
||||
@@ -33,7 +55,10 @@ export function renderEntityTraces({
|
||||
return render(
|
||||
<NuqsTestingAdapter searchParams={searchParams}>
|
||||
<EntityTraces
|
||||
eventEntity="test"
|
||||
timeRange={timeRange}
|
||||
isModalTimeSelection={false}
|
||||
handleTimeChange={jest.fn()}
|
||||
selectedInterval={selectedInterval}
|
||||
queryKey="test"
|
||||
category={category}
|
||||
initialExpression={expression}
|
||||
@@ -76,21 +101,21 @@ export function verifyQueryPayload({
|
||||
expect(orderKeys).toContain('timestamp');
|
||||
}
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/EntityDateTimeSelector', () => ({
|
||||
jest.mock('container/TopNav/DateTimeSelectionV2/index.tsx', () => ({
|
||||
__esModule: true,
|
||||
default: (): JSX.Element => (
|
||||
<div data-testid="mock-datetime-selection">Date Time</div>
|
||||
default: ({
|
||||
onTimeChange,
|
||||
}: {
|
||||
onTimeChange?: (interval: string, dateTimeRange?: [number, number]) => void;
|
||||
}): JSX.Element => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="mock-datetime-selection"
|
||||
onClick={(): void => {
|
||||
onTimeChange?.('5m');
|
||||
}}
|
||||
>
|
||||
Select Time
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('../../EntityDateTimeSelector/useEntityDetailsTime', () => ({
|
||||
useEntityDetailsTime: (): {
|
||||
timeRange: { startTime: number; endTime: number };
|
||||
selectedInterval: string;
|
||||
handleTimeChange: jest.Mock;
|
||||
} => ({
|
||||
timeRange: { startTime: 1, endTime: 2 },
|
||||
selectedInterval: '5m',
|
||||
handleTimeChange: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Container } from '@signozhq/icons';
|
||||
import { InfraMonitoringEvents } from 'constants/events';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
|
||||
import { CustomTab } from '../Base/K8sBaseDetails';
|
||||
@@ -11,20 +10,6 @@ import {
|
||||
|
||||
import EntityMetrics from './EntityMetrics';
|
||||
|
||||
const categoryToEventEntity: Record<InfraMonitoringEntity, string> = {
|
||||
[InfraMonitoringEntity.DAEMONSETS]: InfraMonitoringEvents.DaemonSet,
|
||||
[InfraMonitoringEntity.DEPLOYMENTS]: InfraMonitoringEvents.Deployment,
|
||||
[InfraMonitoringEntity.JOBS]: InfraMonitoringEvents.Job,
|
||||
[InfraMonitoringEntity.NAMESPACES]: InfraMonitoringEvents.Namespace,
|
||||
[InfraMonitoringEntity.STATEFULSETS]: InfraMonitoringEvents.StatefulSet,
|
||||
[InfraMonitoringEntity.PODS]: InfraMonitoringEvents.Pod,
|
||||
[InfraMonitoringEntity.NODES]: InfraMonitoringEvents.Node,
|
||||
[InfraMonitoringEntity.CLUSTERS]: InfraMonitoringEvents.Cluster,
|
||||
[InfraMonitoringEntity.VOLUMES]: InfraMonitoringEvents.Volume,
|
||||
[InfraMonitoringEntity.HOSTS]: InfraMonitoringEvents.HostEntity,
|
||||
[InfraMonitoringEntity.CONTAINERS]: 'container',
|
||||
};
|
||||
|
||||
interface CreatePodMetricsTabParams<T> {
|
||||
getQueryPayload: (
|
||||
entity: T,
|
||||
@@ -32,29 +17,30 @@ interface CreatePodMetricsTabParams<T> {
|
||||
end: number,
|
||||
dotMetricsEnabled: boolean,
|
||||
) => GetQueryResultsProps[];
|
||||
queryKey: string;
|
||||
category: InfraMonitoringEntity;
|
||||
queryKey: string;
|
||||
}
|
||||
|
||||
export function createPodMetricsTab<T>({
|
||||
getQueryPayload,
|
||||
queryKey,
|
||||
category,
|
||||
queryKey,
|
||||
}: CreatePodMetricsTabParams<T>): CustomTab<T> {
|
||||
const eventEntity = categoryToEventEntity[category];
|
||||
|
||||
return {
|
||||
key: VIEW_TYPES.POD_METRICS,
|
||||
label: 'Pod Metrics',
|
||||
icon: <Container size={14} />,
|
||||
render: ({ entity }) => (
|
||||
render: ({ entity, timeRange, selectedInterval, handleTimeChange }) => (
|
||||
<EntityMetrics
|
||||
entity={entity}
|
||||
eventEntity={eventEntity}
|
||||
selectedInterval={selectedInterval}
|
||||
timeRange={timeRange}
|
||||
handleTimeChange={handleTimeChange}
|
||||
isModalTimeSelection
|
||||
entityWidgetInfo={podUtilizationByPodWidgetInfo}
|
||||
getEntityQueryPayload={getQueryPayload}
|
||||
queryKey={queryKey}
|
||||
category={category}
|
||||
queryKey={queryKey}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -454,9 +454,3 @@
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.entity-date-time-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
@@ -905,9 +905,6 @@ export const INFRA_MONITORING_K8S_PARAMS_KEYS = {
|
||||
SELECTED_ITEM: 'selectedItem',
|
||||
SELECTED_ITEM_CLUSTER_NAME: 'selectedItemClusterName',
|
||||
SELECTED_ITEM_NAMESPACE_NAME: 'selectedItemNamespaceName',
|
||||
DETAIL_RELATIVE_TIME: 'detailRelativeTime',
|
||||
DETAIL_START_TIME: 'detailStartTime',
|
||||
DETAIL_END_TIME: 'detailEndTime',
|
||||
};
|
||||
|
||||
/** Metric namespace prefixes for /fields/keys and /fields/values APIs */
|
||||
|
||||
@@ -17,9 +17,12 @@ import { ChevronDown, Dot, PencilLine, Plug, Plus } from '@signozhq/icons';
|
||||
|
||||
import AzureCloudAccountSetupModal from '../../AzureCloudServices/AddNewAccount/CloudAccountSetupModal';
|
||||
import AzureAccountSettingsModal from '../../AzureCloudServices/EditAccount/AccountSettingsModal';
|
||||
import GcpCloudAccountSetupDrawer from '../../GoogleCloudPlatform/AddNewAccount/CloudAccountSetupDrawer';
|
||||
import GcpAccountSettingsDrawer from '../../GoogleCloudPlatform/EditAccount/AccountSettingsDrawer';
|
||||
import {
|
||||
mapAccountDtoToAwsCloudAccount,
|
||||
mapAccountDtoToAzureCloudAccount,
|
||||
mapAccountDtoToGcpCloudAccount,
|
||||
} from '../../mapCloudAccountFromDto';
|
||||
import AwsCloudAccountSetupModal from '../AddNewAccount/CloudAccountSetupModal';
|
||||
import AwsAccountSettingsModal from '../EditAccount/AccountSettingsModal';
|
||||
@@ -156,6 +159,18 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
});
|
||||
}
|
||||
|
||||
if (type === IntegrationType.GCP_SERVICES) {
|
||||
raw.forEach((account) => {
|
||||
if (!account) {
|
||||
return;
|
||||
}
|
||||
const mapped = mapAccountDtoToGcpCloudAccount(account);
|
||||
if (mapped) {
|
||||
mappedAccounts.push(mapped);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return mappedAccounts;
|
||||
}, [listAccountsResponse, type]);
|
||||
|
||||
@@ -207,13 +222,23 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
// log telemetry event when an account is viewed.
|
||||
useEffect(() => {
|
||||
if (activeAccount) {
|
||||
const { config } = activeAccount;
|
||||
let enabledRegions: string[];
|
||||
if ('regions' in config) {
|
||||
// AWS
|
||||
enabledRegions = config.regions;
|
||||
} else if ('resource_groups' in config) {
|
||||
// Azure
|
||||
enabledRegions = config.resource_groups;
|
||||
} else {
|
||||
// GCP
|
||||
enabledRegions = config.project_ids;
|
||||
}
|
||||
|
||||
logEvent(`${type} Integration: Account viewed`, {
|
||||
cloudAccountId: activeAccount?.cloud_account_id,
|
||||
status: activeAccount?.status,
|
||||
enabledRegions:
|
||||
'regions' in activeAccount.config
|
||||
? activeAccount.config.regions
|
||||
: activeAccount.config.resource_groups,
|
||||
enabledRegions,
|
||||
});
|
||||
}
|
||||
}, [activeAccount, type]);
|
||||
@@ -260,6 +285,11 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
onClose={(): void => setIsIntegrationModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
{type === IntegrationType.GCP_SERVICES && (
|
||||
<GcpCloudAccountSetupDrawer
|
||||
onClose={(): void => setIsIntegrationModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -281,6 +311,13 @@ function AccountActions({ type }: { type: IntegrationType }): JSX.Element {
|
||||
setActiveAccount={setActiveAccount}
|
||||
/>
|
||||
)}
|
||||
{type === IntegrationType.GCP_SERVICES && (
|
||||
<GcpAccountSettingsDrawer
|
||||
onClose={(): void => setIsAccountSettingsModalOpen(false)}
|
||||
account={activeAccount}
|
||||
setActiveAccount={setActiveAccount}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -46,14 +46,31 @@ const EMPTY_FORM_VALUES: ServiceConfigFormValues = {
|
||||
s3BucketsByRegion: {},
|
||||
};
|
||||
|
||||
function getIntegrationServiceConfig(
|
||||
type: IntegrationType,
|
||||
serviceDetailsData?: ServiceDetailsData,
|
||||
):
|
||||
| { logs?: { enabled?: boolean }; metrics?: { enabled?: boolean } }
|
||||
| undefined {
|
||||
const config = serviceDetailsData?.cloudIntegrationService?.config;
|
||||
|
||||
if (type === IntegrationType.AWS_SERVICES) {
|
||||
return config?.aws;
|
||||
}
|
||||
if (type === IntegrationType.GCP_SERVICES) {
|
||||
return config?.gcp;
|
||||
}
|
||||
return config?.azure;
|
||||
}
|
||||
|
||||
function getInitialFormValues(
|
||||
type: IntegrationType,
|
||||
serviceDetailsData?: ServiceDetailsData,
|
||||
): ServiceConfigFormValues {
|
||||
const integrationConfig =
|
||||
type === IntegrationType.AWS_SERVICES
|
||||
? serviceDetailsData?.cloudIntegrationService?.config?.aws
|
||||
: serviceDetailsData?.cloudIntegrationService?.config?.azure;
|
||||
const integrationConfig = getIntegrationServiceConfig(
|
||||
type,
|
||||
serviceDetailsData,
|
||||
);
|
||||
|
||||
return {
|
||||
logsEnabled: integrationConfig?.logs?.enabled || false,
|
||||
@@ -98,16 +115,21 @@ function getServiceConfigPayload({
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
azure: {
|
||||
logs: {
|
||||
enabled: isLogsSupported ? logsEnabled : false,
|
||||
},
|
||||
metrics: {
|
||||
enabled: isMetricsSupported ? metricsEnabled : false,
|
||||
},
|
||||
// Azure and GCP share the same simple logs/metrics enable-flag shape.
|
||||
const signalConfig = {
|
||||
logs: {
|
||||
enabled: isLogsSupported ? logsEnabled : false,
|
||||
},
|
||||
metrics: {
|
||||
enabled: isMetricsSupported ? metricsEnabled : false,
|
||||
},
|
||||
};
|
||||
|
||||
if (type === IntegrationType.GCP_SERVICES) {
|
||||
return { gcp: signalConfig };
|
||||
}
|
||||
|
||||
return { azure: signalConfig };
|
||||
}
|
||||
|
||||
function ServiceDetails({
|
||||
@@ -163,10 +185,10 @@ function ServiceDetails({
|
||||
? isAccountServiceLoading
|
||||
: isReadOnlyServiceLoading;
|
||||
|
||||
const integrationConfig =
|
||||
type === IntegrationType.AWS_SERVICES
|
||||
? serviceDetailsData?.cloudIntegrationService?.config?.aws
|
||||
: serviceDetailsData?.cloudIntegrationService?.config?.azure;
|
||||
const integrationConfig = getIntegrationServiceConfig(
|
||||
type,
|
||||
serviceDetailsData,
|
||||
);
|
||||
const isServiceEnabledInPersistedConfig =
|
||||
Boolean(integrationConfig?.logs?.enabled) ||
|
||||
Boolean(integrationConfig?.metrics?.enabled);
|
||||
|
||||
@@ -36,8 +36,12 @@ function AccountSettingsModal({
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// `account.config` is the shared per-provider union (Azure | AWS | GCP).
|
||||
// Narrow to Azure by `resource_groups` (Azure-only) rather than
|
||||
// `deployment_region`, which GCP also has — so it no longer identifies
|
||||
// Azure uniquely.
|
||||
const azureConfig = useMemo(
|
||||
() => ('deployment_region' in account.config ? account.config : null),
|
||||
() => ('resource_groups' in account.config ? account.config : null),
|
||||
[account.config],
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
.setupDrawer {
|
||||
--dialog-header-padding: var(--spacing-10) var(--spacing-12);
|
||||
--dialog-footer-padding: var(--spacing-8) var(--spacing-12);
|
||||
|
||||
// Fill every field with --l2-background (Input/ComboboxSimple default to transparent).
|
||||
--input-background: var(--l2-background);
|
||||
--input-hover-background: var(--l2-background);
|
||||
--input-focus-background: var(--l2-background);
|
||||
--combobox-trigger-background-color: var(--l2-background);
|
||||
|
||||
// Text uses the brighter --l1-foreground, placeholders the duller --l3-foreground.
|
||||
--input-foreground: var(--l1-foreground);
|
||||
--input-placeholder-color: var(--l3-foreground);
|
||||
--combobox-trigger-placeholder-color: var(--l3-foreground);
|
||||
|
||||
// Shared resting border (--l2-border) and text size; focus borders stay per-component.
|
||||
--input-border-color: var(--l2-border);
|
||||
--input-hover-border-color: var(--l2-border);
|
||||
--combobox-trigger-border-color: var(--l2-border);
|
||||
--input-font-size: var(--periscope-font-size-base);
|
||||
--combobox-trigger-font-size: var(--periscope-font-size-base);
|
||||
--combobox-trigger-value-font-size: var(--periscope-font-size-base);
|
||||
|
||||
// Bounded flex column so the header/footer stay put and only the body
|
||||
// scrolls when content overflows.
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
[data-slot='drawer-header'],
|
||||
[data-slot='drawer-footer'] {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
// The drawer body renders inside [data-slot='drawer-description'] — this is
|
||||
// the only region allowed to scroll.
|
||||
[data-slot='drawer-description'] {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-10);
|
||||
min-height: 0;
|
||||
padding: var(--spacing-10) var(--spacing-12);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
[data-slot='select-content'] {
|
||||
width: var(--radix-select-trigger-width);
|
||||
}
|
||||
|
||||
[data-slot='combobox-content'] {
|
||||
z-index: 5;
|
||||
background: var(--l1-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
// Selected region value: bright, like every other field's text.
|
||||
[data-slot='combobox-value'] {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
// Empty trigger (.regionEmpty, set from the RHF value): dull the placeholder text.
|
||||
.regionEmpty [data-slot='combobox-value'] {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: var(--periscope-font-size-medium);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
}
|
||||
|
||||
.footerContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.drawerSection {
|
||||
composes: drawerSection from './shared.module.scss';
|
||||
}
|
||||
|
||||
.mono {
|
||||
composes: mono from './shared.module.scss';
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fieldError {
|
||||
composes: fieldError from './shared.module.scss';
|
||||
}
|
||||
|
||||
.projectIdsSelect {
|
||||
:global(.ant-select-selector) {
|
||||
min-height: 36px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border) !important;
|
||||
}
|
||||
|
||||
&:hover :global(.ant-select-selector),
|
||||
&:global(.ant-select-focused) :global(.ant-select-selector) {
|
||||
border-color: var(--l2-border);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
// antd defaults to 14px; pin to 13px to line up with the Input/Combobox fields.
|
||||
:global(.ant-select-selection-placeholder),
|
||||
:global(.ant-select-selection-search-input),
|
||||
:global(.ant-select-selection-item) {
|
||||
font-size: var(--periscope-font-size-base);
|
||||
}
|
||||
|
||||
:global(.ant-select-selection-placeholder) {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
:global(.ant-select-selection-search-input) {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
:global(.ant-select-selection-item) {
|
||||
color: var(--l1-foreground);
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
:global(.ant-select-selection-item-remove) {
|
||||
color: var(--l3-foreground);
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { Callout } from '@signozhq/ui/callout';
|
||||
import { ComboboxSimple } from '@signozhq/ui/combobox';
|
||||
import { DrawerWrapper } from '@signozhq/ui/drawer';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Select } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { GCP_REGIONS } from 'container/Integrations/constants';
|
||||
import { IntegrationModalProps } from 'container/Integrations/HeroSection/types';
|
||||
import { useCloudAccountSetupDrawer } from 'hooks/integration/gcp/useCloudAccountSetupDrawer';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import ConnectionSecretsFields from './ConnectionSecretsFields';
|
||||
import FieldLabel from './FieldLabel';
|
||||
import FlowSelector from './FlowSelector';
|
||||
import { GcpSetupFormValues, SetupFlow } from './types';
|
||||
|
||||
import styles from './CloudAccountSetupDrawer.module.scss';
|
||||
|
||||
const REGION_ITEMS = GCP_REGIONS.map((region) => ({
|
||||
value: region.value,
|
||||
label: `${region.label} (${region.value})`,
|
||||
}));
|
||||
|
||||
const DEFAULT_VALUES: GcpSetupFormValues = {
|
||||
accountName: '',
|
||||
deploymentProjectId: '',
|
||||
deploymentRegion: '',
|
||||
projectIds: [],
|
||||
sigNozApiUrl: '',
|
||||
sigNozApiKey: '',
|
||||
ingestionUrl: '',
|
||||
ingestionKey: '',
|
||||
};
|
||||
|
||||
function CloudAccountSetupDrawer({
|
||||
onClose,
|
||||
}: IntegrationModalProps): JSX.Element {
|
||||
const {
|
||||
isLoading,
|
||||
connectAccount,
|
||||
handleClose,
|
||||
connectionParams,
|
||||
isConnectionParamsLoading,
|
||||
submitError,
|
||||
clearSubmitError,
|
||||
} = useCloudAccountSetupDrawer({ onClose });
|
||||
|
||||
const { control, handleSubmit, setValue } = useForm<GcpSetupFormValues>({
|
||||
defaultValues: DEFAULT_VALUES,
|
||||
});
|
||||
|
||||
const [flow, setFlow] = useState<SetupFlow>('manual');
|
||||
|
||||
// Pre-fill the deployment/ingestion fields with the fetched credentials.
|
||||
useEffect(() => {
|
||||
if (!connectionParams) {
|
||||
return;
|
||||
}
|
||||
setValue('sigNozApiUrl', connectionParams.sigNozApiUrl);
|
||||
setValue('sigNozApiKey', connectionParams.sigNozApiKey);
|
||||
setValue('ingestionUrl', connectionParams.ingestionUrl);
|
||||
setValue('ingestionKey', connectionParams.ingestionKey);
|
||||
}, [connectionParams, setValue]);
|
||||
|
||||
const footer = (
|
||||
<div className={styles.footerContainer}>
|
||||
{submitError && (
|
||||
<Callout
|
||||
type="error"
|
||||
size="small"
|
||||
showIcon
|
||||
action="dismissible"
|
||||
onClick={clearSubmitError}
|
||||
title="Failed to connect GCP account"
|
||||
testId="gcp-connect-error"
|
||||
>
|
||||
{submitError}
|
||||
</Callout>
|
||||
)}
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
onClick={handleClose}
|
||||
testId="gcp-cancel-btn"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
onClick={handleSubmit(connectAccount)}
|
||||
loading={isLoading}
|
||||
disabled={isConnectionParamsLoading}
|
||||
testId="gcp-connect-account-btn"
|
||||
>
|
||||
Connect Account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={true}
|
||||
className={styles.setupDrawer}
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
direction="right"
|
||||
showCloseButton
|
||||
title="Connect Google Cloud Platform"
|
||||
width="base"
|
||||
footer={footer}
|
||||
drawerHeaderProps={{ className: styles.title }}
|
||||
>
|
||||
<FlowSelector value={flow} onChange={setFlow} />
|
||||
<div className={styles.drawerSection}>
|
||||
<FieldLabel
|
||||
htmlFor="gcp-account-name-input"
|
||||
label="Account Name"
|
||||
tooltip="A label to identify this group of GCP projects (org ID, billing email, or any descriptive name)"
|
||||
required
|
||||
/>
|
||||
<Controller
|
||||
name="accountName"
|
||||
control={control}
|
||||
rules={{ required: 'Please enter an account name' }}
|
||||
render={({ field, fieldState }): JSX.Element => (
|
||||
<>
|
||||
<Input
|
||||
id="gcp-account-name-input"
|
||||
className={styles.fullWidth}
|
||||
placeholder="e.g. my-org or billing@company.com"
|
||||
value={field.value}
|
||||
onChange={(e): void => field.onChange(e.target.value)}
|
||||
testId="gcp-account-name-input"
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<Typography.Text
|
||||
as="span"
|
||||
size="small"
|
||||
role="alert"
|
||||
className={styles.fieldError}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.drawerSection}>
|
||||
<FieldLabel
|
||||
htmlFor="gcp-deployment-project-id-input"
|
||||
label="Deployment Project ID"
|
||||
tooltip="The GCP project that hosts your OTel Collector deployment — often separate from the projects you actually monitor"
|
||||
required
|
||||
/>
|
||||
<Controller
|
||||
name="deploymentProjectId"
|
||||
control={control}
|
||||
rules={{ required: 'Please enter the deployment project ID' }}
|
||||
render={({ field, fieldState }): JSX.Element => (
|
||||
<>
|
||||
<Input
|
||||
id="gcp-deployment-project-id-input"
|
||||
className={cx(styles.fullWidth, styles.mono)}
|
||||
placeholder="e.g. my-deployment-project-123"
|
||||
value={field.value}
|
||||
onChange={(e): void => field.onChange(e.target.value)}
|
||||
testId="gcp-deployment-project-id-input"
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<Typography.Text
|
||||
as="span"
|
||||
size="small"
|
||||
role="alert"
|
||||
className={styles.fieldError}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.drawerSection}>
|
||||
<FieldLabel
|
||||
htmlFor="gcp-deployment-region-select"
|
||||
label="Deployment Region"
|
||||
tooltip="The GCP region where your OTel Collector will be deployed"
|
||||
required
|
||||
/>
|
||||
<Controller
|
||||
name="deploymentRegion"
|
||||
control={control}
|
||||
rules={{ required: 'Please select a region' }}
|
||||
render={({ field, fieldState }): JSX.Element => (
|
||||
<>
|
||||
<ComboboxSimple
|
||||
id="gcp-deployment-region-select"
|
||||
className={cx(styles.fullWidth, {
|
||||
[styles.regionEmpty]: !field.value,
|
||||
})}
|
||||
items={REGION_ITEMS}
|
||||
value={field.value}
|
||||
onChange={(value): void => field.onChange(value as string)}
|
||||
placeholder="Select a region..."
|
||||
inputPlaceholder="Search regions…"
|
||||
withPortal={false}
|
||||
testId="gcp-deployment-region-select"
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<Typography.Text
|
||||
as="span"
|
||||
size="small"
|
||||
role="alert"
|
||||
className={styles.fieldError}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.drawerSection}>
|
||||
<FieldLabel
|
||||
htmlFor="gcp-project-ids-select"
|
||||
label="Projects to Monitor"
|
||||
tooltip="Enter each GCP project ID then press Enter"
|
||||
required
|
||||
/>
|
||||
<Controller
|
||||
name="projectIds"
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value): true | string =>
|
||||
value.length > 0 || 'Please add at least one project ID',
|
||||
}}
|
||||
render={({ field, fieldState }): JSX.Element => (
|
||||
<>
|
||||
<Select
|
||||
id="gcp-project-ids-select"
|
||||
className={cx(styles.fullWidth, styles.projectIdsSelect)}
|
||||
mode="tags"
|
||||
value={field.value}
|
||||
onChange={(value): void => field.onChange(value)}
|
||||
placeholder="Add project IDs…"
|
||||
tokenSeparators={[',', ' ']}
|
||||
notFoundContent={null}
|
||||
suffixIcon={null}
|
||||
getPopupContainer={popupContainer}
|
||||
data-testid="gcp-project-ids-select"
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<Typography.Text
|
||||
as="span"
|
||||
size="small"
|
||||
role="alert"
|
||||
className={styles.fieldError}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<ConnectionSecretsFields
|
||||
control={control}
|
||||
isLoading={isConnectionParamsLoading}
|
||||
connectionParams={connectionParams}
|
||||
/>
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default CloudAccountSetupDrawer;
|
||||
@@ -0,0 +1,71 @@
|
||||
.drawerSurface {
|
||||
composes: drawerSurface from './shared.module.scss';
|
||||
}
|
||||
|
||||
.drawerSurfaceHead {
|
||||
composes: drawerSurfaceHead from './shared.module.scss';
|
||||
}
|
||||
|
||||
.drawerSection {
|
||||
composes: drawerSection from './shared.module.scss';
|
||||
}
|
||||
|
||||
.headLabel {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.mono {
|
||||
composes: mono from './shared.module.scss';
|
||||
}
|
||||
|
||||
.fieldError {
|
||||
composes: fieldError from './shared.module.scss';
|
||||
}
|
||||
|
||||
.fullWidth {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.secretsBody {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-6);
|
||||
}
|
||||
|
||||
.skeletonLabel :global(.ant-skeleton-input) {
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.skeletonInput :global(.ant-skeleton-input) {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.readonlyField {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
align-items: center;
|
||||
height: 36px;
|
||||
padding: 0 var(--spacing-2) 0 var(--spacing-4);
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
border-radius: var(--radius-2);
|
||||
}
|
||||
|
||||
.readonlyValue {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
// Match the other fields' value text: 13px and the brighter --l1-foreground.
|
||||
font-size: var(--periscope-font-size-base);
|
||||
color: var(--l2-foreground);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { Lock } from '@signozhq/icons';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { Skeleton } from 'antd';
|
||||
import { CloudintegrationtypesCredentialsDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import cx from 'classnames';
|
||||
import CopyButton from 'periscope/components/CopyButton/CopyButton';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
|
||||
import FieldLabel from './FieldLabel';
|
||||
import { GcpSetupFormValues } from './types';
|
||||
import { SecretFieldType, validateSecretValue } from './validators';
|
||||
import styles from './ConnectionSecretsFields.module.scss';
|
||||
|
||||
type CredentialField = keyof CloudintegrationtypesCredentialsDTO;
|
||||
|
||||
interface FieldConfig {
|
||||
name: CredentialField;
|
||||
label: string;
|
||||
tooltip: string;
|
||||
placeholder: string;
|
||||
testId: string;
|
||||
type: SecretFieldType;
|
||||
}
|
||||
|
||||
const FIELDS: FieldConfig[] = [
|
||||
{
|
||||
name: 'sigNozApiUrl',
|
||||
label: 'SigNoz API URL',
|
||||
tooltip: 'Base URL of your SigNoz instance the collector reports to',
|
||||
placeholder: 'https://<tenant>.signoz.cloud',
|
||||
testId: 'gcp-signoz-api-url-input',
|
||||
type: 'url',
|
||||
},
|
||||
{
|
||||
name: 'sigNozApiKey',
|
||||
label: 'SigNoz API Key',
|
||||
tooltip: 'API key used to authenticate with your SigNoz instance',
|
||||
placeholder: 'Enter SigNoz API key',
|
||||
testId: 'gcp-signoz-api-key-input',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'ingestionUrl',
|
||||
label: 'Ingestion URL',
|
||||
tooltip: 'OTLP ingestion endpoint your OTel Collector sends telemetry to',
|
||||
placeholder: 'https://ingest.<region>.signoz.cloud',
|
||||
testId: 'gcp-ingestion-url-input',
|
||||
type: 'url',
|
||||
},
|
||||
{
|
||||
name: 'ingestionKey',
|
||||
label: 'Ingestion Key',
|
||||
tooltip: 'Ingestion key that authorizes telemetry sent to SigNoz',
|
||||
placeholder: 'Enter ingestion key',
|
||||
testId: 'gcp-ingestion-key-input',
|
||||
type: 'text',
|
||||
},
|
||||
];
|
||||
|
||||
interface ConnectionSecretsFieldsProps {
|
||||
control: Control<GcpSetupFormValues>;
|
||||
isLoading: boolean;
|
||||
connectionParams?: CloudintegrationtypesCredentialsDTO;
|
||||
}
|
||||
|
||||
function ConnectionSecretsFields({
|
||||
control,
|
||||
isLoading,
|
||||
connectionParams,
|
||||
}: ConnectionSecretsFieldsProps): JSX.Element {
|
||||
const hasMissingValue = FIELDS.some(
|
||||
(field) => !connectionParams?.[field.name],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.drawerSurface}>
|
||||
<div className={styles.drawerSurfaceHead}>
|
||||
<Typography.Text weight="bold" size="base">
|
||||
Deployment details & ingestion secrets
|
||||
</Typography.Text>
|
||||
{!hasMissingValue && (
|
||||
<div className={styles.headLabel}>
|
||||
<Lock size={12} />
|
||||
<Typography.Text as="span" size="small" className={styles.headLabel}>
|
||||
Auto-filled by SigNoz
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className={styles.secretsBody} data-testid="gcp-secrets-skeleton">
|
||||
{FIELDS.map((field) => (
|
||||
<div key={field.name} className={styles.drawerSection}>
|
||||
<Skeleton.Input active size="small" className={styles.skeletonLabel} />
|
||||
<Skeleton.Input active block className={styles.skeletonInput} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.secretsBody}>
|
||||
{FIELDS.map((field) => {
|
||||
// Backend-provided values are read-only — the user can't edit them, so
|
||||
// show a truncated value with a copy button. Missing values (enterprise)
|
||||
// stay editable inputs with no copy button.
|
||||
const providedValue = connectionParams?.[field.name];
|
||||
if (providedValue) {
|
||||
return (
|
||||
<div key={field.name} className={styles.drawerSection}>
|
||||
<FieldLabel
|
||||
htmlFor={field.testId}
|
||||
label={field.label}
|
||||
tooltip={field.tooltip}
|
||||
/>
|
||||
<div className={styles.readonlyField}>
|
||||
<Typography.Text
|
||||
as="span"
|
||||
id={field.testId}
|
||||
className={cx(styles.readonlyValue, styles.mono)}
|
||||
title={providedValue}
|
||||
testId={field.testId}
|
||||
>
|
||||
{providedValue}
|
||||
</Typography.Text>
|
||||
<CopyButton
|
||||
value={providedValue}
|
||||
size={12}
|
||||
ariaLabel={`Copy ${field.label}`}
|
||||
testId={`${field.testId}-copy`}
|
||||
onCopy={(): void => {
|
||||
toast.success(`${field.label} copied to clipboard`, {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={field.name} className={styles.drawerSection}>
|
||||
<FieldLabel
|
||||
htmlFor={field.testId}
|
||||
label={field.label}
|
||||
tooltip={field.tooltip}
|
||||
/>
|
||||
<Controller
|
||||
name={field.name}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value): true | string =>
|
||||
validateSecretValue(field.label, field.type, value),
|
||||
}}
|
||||
render={({ field: rhfField, fieldState }): JSX.Element => (
|
||||
<>
|
||||
<Input
|
||||
id={field.testId}
|
||||
className={cx(styles.fullWidth, styles.mono)}
|
||||
placeholder={field.placeholder}
|
||||
value={rhfField.value}
|
||||
onChange={(e): void => rhfField.onChange(e.target.value)}
|
||||
testId={field.testId}
|
||||
/>
|
||||
{fieldState.error && (
|
||||
<Typography.Text
|
||||
as="span"
|
||||
size="small"
|
||||
role="alert"
|
||||
className={styles.fieldError}
|
||||
>
|
||||
{fieldState.error.message}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ConnectionSecretsFields.defaultProps = {
|
||||
connectionParams: undefined,
|
||||
};
|
||||
|
||||
export default ConnectionSecretsFields;
|
||||
@@ -0,0 +1,22 @@
|
||||
.fieldLabel {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
|
||||
.required {
|
||||
composes: required from './shared.module.scss';
|
||||
}
|
||||
|
||||
.infoTrigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: var(--l3-foreground);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.tooltipContent {
|
||||
max-width: 240px;
|
||||
white-space: normal;
|
||||
word-break: break-word;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Info } from '@signozhq/icons';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
|
||||
import styles from './FieldLabel.module.scss';
|
||||
|
||||
interface FieldLabelProps {
|
||||
htmlFor: string;
|
||||
label: string;
|
||||
tooltip: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
function FieldLabel({
|
||||
htmlFor,
|
||||
label,
|
||||
tooltip,
|
||||
required,
|
||||
}: FieldLabelProps): JSX.Element {
|
||||
return (
|
||||
<label className={styles.fieldLabel} htmlFor={htmlFor}>
|
||||
{label}
|
||||
|
||||
<TooltipSimple
|
||||
title={tooltip}
|
||||
side="top"
|
||||
tooltipContentProps={{ className: styles.tooltipContent }}
|
||||
>
|
||||
<span
|
||||
className={styles.infoTrigger}
|
||||
aria-label={`${label} help`}
|
||||
data-testid={`${htmlFor}-tooltip`}
|
||||
>
|
||||
<Info size={12} />
|
||||
</span>
|
||||
</TooltipSimple>
|
||||
{required && (
|
||||
<span className={styles.required} aria-hidden="true">
|
||||
*
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
FieldLabel.defaultProps = {
|
||||
required: false,
|
||||
};
|
||||
|
||||
export default FieldLabel;
|
||||
@@ -0,0 +1,84 @@
|
||||
.drawerSection {
|
||||
composes: drawerSection from './shared.module.scss';
|
||||
}
|
||||
|
||||
.drawerSurface {
|
||||
composes: drawerSurface from './shared.module.scss';
|
||||
}
|
||||
|
||||
.drawerSurfaceHead {
|
||||
composes: drawerSurfaceHead from './shared.module.scss';
|
||||
}
|
||||
|
||||
.flowRadioGroup {
|
||||
--radio-group-item-border-color: var(--l2-border);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4);
|
||||
width: 100%;
|
||||
|
||||
.flowRadio {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: var(--spacing-5);
|
||||
width: 100%;
|
||||
padding: var(--spacing-5) var(--spacing-6);
|
||||
margin: 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-2);
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
background-color 0.12s ease,
|
||||
border-color 0.12s ease;
|
||||
|
||||
> button[role='radio'] {
|
||||
flex: 0 0 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
> label {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
display: block;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&.flowRadioManual:has(button[data-state='checked']) {
|
||||
background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
|
||||
border-color: color-mix(in srgb, var(--accent-primary) 30%, transparent);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background-hover);
|
||||
}
|
||||
|
||||
&:has(button[disabled]) {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.flowRadioTitle {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flowRadioDesc {
|
||||
margin-top: var(--spacing-2);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { RadioGroup, RadioGroupItem } from '@signozhq/ui/radio-group';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
|
||||
import { SetupFlow } from './types';
|
||||
import styles from './FlowSelector.module.scss';
|
||||
|
||||
interface FlowSelectorProps {
|
||||
value: SetupFlow;
|
||||
onChange: (flow: SetupFlow) => void;
|
||||
}
|
||||
|
||||
function FlowSelector({ value, onChange }: FlowSelectorProps): JSX.Element {
|
||||
return (
|
||||
<div className={cx(styles.drawerSection, styles.drawerSurface)}>
|
||||
<div className={styles.drawerSurfaceHead}>
|
||||
<Typography.Text weight="bold" size="base">
|
||||
Connection method
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
<RadioGroup
|
||||
value={value}
|
||||
onChange={(next): void => onChange(next as SetupFlow)}
|
||||
className={styles.flowRadioGroup}
|
||||
>
|
||||
<RadioGroupItem
|
||||
value="manual"
|
||||
containerClassName={cx(styles.flowRadio, styles.flowRadioManual)}
|
||||
testId="gcp-flow-manual"
|
||||
>
|
||||
<div className={styles.flowRadioTitle}>
|
||||
<Typography.Text weight="semibold" size="base">
|
||||
Connect Manually
|
||||
</Typography.Text>
|
||||
</div>
|
||||
<Typography.Text
|
||||
as="p"
|
||||
size="small"
|
||||
color="muted"
|
||||
className={styles.flowRadioDesc}
|
||||
>
|
||||
Deploy your own OTel Collector and configure log sinks.
|
||||
</Typography.Text>
|
||||
</RadioGroupItem>
|
||||
|
||||
<RadioGroupItem
|
||||
value="agent"
|
||||
containerClassName={styles.flowRadio}
|
||||
testId="gcp-flow-agent"
|
||||
disabled
|
||||
>
|
||||
<div className={styles.flowRadioTitle}>
|
||||
<Typography.Text weight="semibold" size="base">
|
||||
Connect via Agent
|
||||
</Typography.Text>
|
||||
<Badge color="robin" variant="default">
|
||||
Soon
|
||||
</Badge>
|
||||
</div>
|
||||
<Typography.Text
|
||||
as="p"
|
||||
size="small"
|
||||
color="muted"
|
||||
className={styles.flowRadioDesc}
|
||||
>
|
||||
SigNoz deploys and manages the collector for you.
|
||||
</Typography.Text>
|
||||
</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default FlowSelector;
|
||||
@@ -0,0 +1,36 @@
|
||||
.drawerSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
}
|
||||
|
||||
.drawerSection > label {
|
||||
font-size: var(--periscope-font-size-normal);
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--accent-cherry);
|
||||
}
|
||||
|
||||
.fieldError {
|
||||
font-size: var(--periscope-font-size-small);
|
||||
color: var(--accent-cherry);
|
||||
}
|
||||
|
||||
.drawerSurface {
|
||||
padding: var(--spacing-7);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
.drawerSurfaceHead {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-5);
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--font-family-mono);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export type SetupFlow = 'manual' | 'agent';
|
||||
|
||||
export interface GcpSetupFormValues {
|
||||
accountName: string;
|
||||
deploymentProjectId: string;
|
||||
deploymentRegion: string;
|
||||
projectIds: string[];
|
||||
sigNozApiUrl: string;
|
||||
sigNozApiKey: string;
|
||||
ingestionUrl: string;
|
||||
ingestionKey: string;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export type SecretFieldType = 'url' | 'text';
|
||||
|
||||
export function isValidUrl(value: string): boolean {
|
||||
try {
|
||||
return Boolean(new URL(value));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function validateSecretValue(
|
||||
label: string,
|
||||
type: SecretFieldType,
|
||||
value: string | undefined,
|
||||
): true | string {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return `Please enter the ${label}`;
|
||||
}
|
||||
if (type === 'url' && !isValidUrl(trimmed)) {
|
||||
return `Please enter a valid URL for ${label}`;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Dispatch, SetStateAction, useMemo } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { Save } from '@signozhq/icons';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { DrawerWrapper } from '@signozhq/ui/drawer';
|
||||
import { Form, Select } from 'antd';
|
||||
import { invalidateListAccounts } from 'api/generated/services/cloudintegration';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { CloudAccount } from 'container/Integrations/types';
|
||||
import { useAccountSettingsDrawer } from 'hooks/integration/gcp/useAccountSettingsDrawer';
|
||||
|
||||
import RemoveIntegrationAccount from '../../RemoveAccount/RemoveIntegrationAccount';
|
||||
|
||||
import '../../AmazonWebServices/EditAccount/AccountSettingsModal.style.scss';
|
||||
|
||||
interface AccountSettingsDrawerProps {
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
function AccountSettingsDrawer({
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: AccountSettingsDrawerProps): JSX.Element {
|
||||
const {
|
||||
form,
|
||||
isLoading,
|
||||
projectIds,
|
||||
isSaveDisabled,
|
||||
setProjectIds,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
} = useAccountSettingsDrawer({ onClose, account, setActiveAccount });
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const gcpConfig = useMemo(
|
||||
() => ('project_ids' in account.config ? account.config : null),
|
||||
[account.config],
|
||||
);
|
||||
|
||||
return (
|
||||
<DrawerWrapper
|
||||
open={true}
|
||||
className="account-settings-modal"
|
||||
title="Account Settings"
|
||||
direction="right"
|
||||
showCloseButton
|
||||
onOpenChange={(open): void => {
|
||||
if (!open) {
|
||||
handleClose();
|
||||
}
|
||||
}}
|
||||
width="wide"
|
||||
footer={
|
||||
<div className="account-settings-modal__footer">
|
||||
<RemoveIntegrationAccount
|
||||
accountId={account?.id}
|
||||
onRemoveIntegrationAccountSuccess={(): void => {
|
||||
void invalidateListAccounts(queryClient, {
|
||||
cloudProvider: INTEGRATION_TYPES.GCP,
|
||||
});
|
||||
setActiveAccount(null);
|
||||
handleClose();
|
||||
}}
|
||||
cloudProvider={INTEGRATION_TYPES.GCP}
|
||||
/>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
disabled={isSaveDisabled}
|
||||
onClick={handleSubmit}
|
||||
loading={isLoading}
|
||||
prefix={<Save size={14} />}
|
||||
data-testid="gcp-update-account-btn"
|
||||
>
|
||||
Update Changes
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
projectIds: gcpConfig?.project_ids || [],
|
||||
}}
|
||||
>
|
||||
<div className="account-settings-modal__body">
|
||||
<div className="account-settings-modal__body-account-info">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details">
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-title">
|
||||
Connected Account details
|
||||
</div>
|
||||
<div className="account-settings-modal__body-account-info-connected-account-details-account-id">
|
||||
Account Name:{' '}
|
||||
<span className="account-settings-modal__body-account-info-connected-account-details-account-id-account-id">
|
||||
{account?.providerAccountId}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{gcpConfig?.deployment_project_id && (
|
||||
<div className="account-settings-modal__body-region-selector">
|
||||
<div className="account-settings-modal__body-region-selector-title">
|
||||
Deployment project ID
|
||||
</div>
|
||||
<div className="account-settings-modal__body-region-selector-description">
|
||||
{gcpConfig.deployment_project_id}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{gcpConfig?.deployment_region && (
|
||||
<div className="account-settings-modal__body-region-selector">
|
||||
<div className="account-settings-modal__body-region-selector-title">
|
||||
Deployment region
|
||||
</div>
|
||||
<div className="account-settings-modal__body-region-selector-description">
|
||||
{gcpConfig.deployment_region}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="account-settings-modal__body-region-selector">
|
||||
<div className="account-settings-modal__body-region-selector-title">
|
||||
Projects to monitor
|
||||
</div>
|
||||
<div className="account-settings-modal__body-region-selector-description">
|
||||
Update the GCP project IDs that should be monitored.
|
||||
</div>
|
||||
|
||||
<Form.Item
|
||||
name="projectIds"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
type: 'array',
|
||||
min: 1,
|
||||
message: 'Please add at least one project ID',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
value={projectIds}
|
||||
tokenSeparators={[',']}
|
||||
onChange={(values): void => {
|
||||
setProjectIds(values);
|
||||
form.setFieldValue('projectIds', values);
|
||||
}}
|
||||
data-testid="gcp-edit-project-ids-select"
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</DrawerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountSettingsDrawer;
|
||||
@@ -60,6 +60,44 @@ function RemoveIntegrationAccount({
|
||||
setIsModalOpen(false);
|
||||
};
|
||||
|
||||
let modalDescription: JSX.Element;
|
||||
if (cloudProvider === INTEGRATION_TYPES.AWS) {
|
||||
modalDescription = (
|
||||
<>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your AWS account within the next ~15 minutes
|
||||
(cloudformation stacks named signoz-integration-telemetry-collection in
|
||||
enabled regions). <br />
|
||||
<br />
|
||||
After that, you can delete the cloudformation stack that was created
|
||||
manually when connecting this account.
|
||||
</>
|
||||
);
|
||||
} else if (cloudProvider === INTEGRATION_TYPES.GCP) {
|
||||
modalDescription = (
|
||||
<>
|
||||
Removing this account will stop SigNoz from monitoring it. <br />
|
||||
<br />
|
||||
Since you manage the GCP resources yourself, remember to manually tear down
|
||||
the OTel collector and Pub/Sub resources you created for this integration if
|
||||
you no longer need them.
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
modalDescription = (
|
||||
<>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your Azure subscription within the next ~15 minutes
|
||||
(deployment stack named signoz-integration-telemetry will be deleted
|
||||
automatically). <br />
|
||||
<br />
|
||||
After that, you have to manually delete 'signoz-integration'
|
||||
deployment stack that was created while connecting this account (Takes ~20
|
||||
minutes to delete).
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="remove-integration-account-container">
|
||||
<Button
|
||||
@@ -84,28 +122,7 @@ function RemoveIntegrationAccount({
|
||||
loading: isRemoveIntegrationLoading,
|
||||
}}
|
||||
>
|
||||
{cloudProvider === INTEGRATION_TYPES.AWS ? (
|
||||
<>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your AWS account within the next ~15 minutes
|
||||
(cloudformation stacks named signoz-integration-telemetry-collection in
|
||||
enabled regions). <br />
|
||||
<br />
|
||||
After that, you can delete the cloudformation stack that was created
|
||||
manually when connecting this account.
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Removing this account will remove all components created for sending
|
||||
telemetry to SigNoz in your Azure subscription within the next ~15 minutes
|
||||
(deployment stack named signoz-integration-telemetry will be deleted
|
||||
automatically). <br />
|
||||
<br />
|
||||
After that, you have to manually delete 'signoz-integration'
|
||||
deployment stack that was created while connecting this account (Takes ~20
|
||||
minutes to delete).
|
||||
</>
|
||||
)}
|
||||
{modalDescription}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -47,3 +47,27 @@ export function mapAccountDtoToAzureCloudAccount(
|
||||
providerAccountId: account.providerAccountId,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapAccountDtoToGcpCloudAccount(
|
||||
account: CloudintegrationtypesAccountDTO,
|
||||
): IntegrationCloudAccount | null {
|
||||
if (!account.providerAccountId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
cloud_account_id: account.id,
|
||||
config: {
|
||||
deployment_region: account.config?.gcp?.deploymentRegion ?? '',
|
||||
deployment_project_id: account.config?.gcp?.deploymentProjectId ?? '',
|
||||
project_ids: account.config?.gcp?.projectIds ?? [],
|
||||
},
|
||||
status: {
|
||||
integration: {
|
||||
last_heartbeat_ts_ms: account.agentReport?.timestampMillis ?? 0,
|
||||
},
|
||||
},
|
||||
providerAccountId: account.providerAccountId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ import { ArrowLeft, MoveUpRight, RotateCw } from '@signozhq/icons';
|
||||
import awwSnapUrl from '@/assets/Icons/awwSnap.svg';
|
||||
|
||||
import CloudIntegration from '../CloudIntegration/CloudIntegration';
|
||||
import { INTEGRATION_TYPES } from '../constants';
|
||||
import { IntegrationType } from '../types';
|
||||
import { INTEGRATION_TYPES } from '../constants';
|
||||
import { handleContactSupport } from '../utils';
|
||||
import IntegrationDetailContent from './IntegrationDetailContent';
|
||||
import IntegrationDetailHeader from './IntegrationDetailHeader';
|
||||
@@ -24,6 +24,12 @@ import { getConnectionStatesFromConnectionStatus } from './utils';
|
||||
|
||||
import './IntegrationDetailPage.styles.scss';
|
||||
|
||||
const cloudIntegrationTypeById: Record<string, IntegrationType> = {
|
||||
[INTEGRATION_TYPES.AWS]: IntegrationType.AWS_SERVICES,
|
||||
[INTEGRATION_TYPES.AZURE]: IntegrationType.AZURE_SERVICES,
|
||||
[INTEGRATION_TYPES.GCP]: IntegrationType.GCP_SERVICES,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
function IntegrationDetailPage(): JSX.Element {
|
||||
const history = useHistory();
|
||||
@@ -55,19 +61,8 @@ function IntegrationDetailPage(): JSX.Element {
|
||||
),
|
||||
);
|
||||
|
||||
if (
|
||||
integrationId === INTEGRATION_TYPES.AWS ||
|
||||
integrationId === INTEGRATION_TYPES.AZURE
|
||||
) {
|
||||
return (
|
||||
<CloudIntegration
|
||||
type={
|
||||
integrationId === INTEGRATION_TYPES.AWS
|
||||
? IntegrationType.AWS_SERVICES
|
||||
: IntegrationType.AZURE_SERVICES
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (integrationId && cloudIntegrationTypeById[integrationId]) {
|
||||
return <CloudIntegration type={cloudIntegrationTypeById[integrationId]} />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import awsDarkLogo from '@/assets/Logos/aws-dark.svg';
|
||||
import azureOpenaiLogo from '@/assets/Logos/azure-openai.svg';
|
||||
import gcpLogo from '@/assets/Logos/gcp.svg';
|
||||
|
||||
import { AzureRegion } from './types';
|
||||
import { AzureRegion, GCPRegion } from './types';
|
||||
|
||||
export const INTEGRATION_TELEMETRY_EVENTS = {
|
||||
INTEGRATIONS_LIST_VISITED: 'Integrations Page: Visited the list page',
|
||||
@@ -21,6 +22,7 @@ export const INTEGRATION_TELEMETRY_EVENTS = {
|
||||
export const INTEGRATION_TYPES = {
|
||||
AWS: 'aws',
|
||||
AZURE: 'azure',
|
||||
GCP: 'gcp',
|
||||
};
|
||||
|
||||
export const AWS_INTEGRATION = {
|
||||
@@ -53,7 +55,26 @@ export const AZURE_INTEGRATION = {
|
||||
is_new: true,
|
||||
};
|
||||
|
||||
export const ONE_CLICK_INTEGRATIONS = [AWS_INTEGRATION, AZURE_INTEGRATION];
|
||||
export const GCP_INTEGRATION = {
|
||||
id: INTEGRATION_TYPES.GCP,
|
||||
title: 'Google Cloud Platform',
|
||||
description: 'Setup for GCP monitoring with SigNoz',
|
||||
author: {
|
||||
name: 'SigNoz',
|
||||
email: 'integrations@signoz.io',
|
||||
homepage: 'https://signoz.io',
|
||||
},
|
||||
icon: gcpLogo,
|
||||
icon_alt: 'gcp-logo',
|
||||
is_installed: false,
|
||||
is_new: true,
|
||||
};
|
||||
|
||||
export const ONE_CLICK_INTEGRATIONS = [
|
||||
AWS_INTEGRATION,
|
||||
AZURE_INTEGRATION,
|
||||
GCP_INTEGRATION,
|
||||
];
|
||||
|
||||
export const AZURE_REGIONS: AzureRegion[] = [
|
||||
{
|
||||
@@ -165,3 +186,66 @@ export const AZURE_REGIONS: AzureRegion[] = [
|
||||
{ label: 'West US 2', value: 'westus2', geography: 'United States' },
|
||||
{ label: 'West US 3', value: 'westus3', geography: 'United States' },
|
||||
];
|
||||
|
||||
// Source of truth: pkg/types/cloudintegrationtypes/regions.go (GCP regions).
|
||||
export const GCP_REGIONS: GCPRegion[] = [
|
||||
{ label: 'Johannesburg', value: 'africa-south1', geography: 'Africa' },
|
||||
{ label: 'Changhua County', value: 'asia-east1', geography: 'APAC' },
|
||||
{ label: 'Hong Kong', value: 'asia-east2', geography: 'APAC' },
|
||||
{ label: 'Tokyo', value: 'asia-northeast1', geography: 'APAC' },
|
||||
{ label: 'Osaka', value: 'asia-northeast2', geography: 'APAC' },
|
||||
{ label: 'Seoul', value: 'asia-northeast3', geography: 'APAC' },
|
||||
{ label: 'Mumbai', value: 'asia-south1', geography: 'APAC' },
|
||||
{ label: 'Delhi', value: 'asia-south2', geography: 'APAC' },
|
||||
{ label: 'Singapore', value: 'asia-southeast1', geography: 'APAC' },
|
||||
{ label: 'Jakarta', value: 'asia-southeast2', geography: 'APAC' },
|
||||
{ label: 'Bangkok', value: 'asia-southeast3', geography: 'APAC' },
|
||||
{ label: 'Sydney', value: 'australia-southeast1', geography: 'APAC' },
|
||||
{ label: 'Melbourne', value: 'australia-southeast2', geography: 'APAC' },
|
||||
{ label: 'Warsaw', value: 'europe-central2', geography: 'Europe' },
|
||||
{ label: 'Hamina', value: 'europe-north1', geography: 'Europe' },
|
||||
{ label: 'Stockholm', value: 'europe-north2', geography: 'Europe' },
|
||||
{ label: 'Madrid', value: 'europe-southwest1', geography: 'Europe' },
|
||||
{ label: 'St. Ghislain', value: 'europe-west1', geography: 'Europe' },
|
||||
{ label: 'London', value: 'europe-west2', geography: 'Europe' },
|
||||
{ label: 'Frankfurt', value: 'europe-west3', geography: 'Europe' },
|
||||
{ label: 'Eemshaven', value: 'europe-west4', geography: 'Europe' },
|
||||
{ label: 'Zurich', value: 'europe-west6', geography: 'Europe' },
|
||||
{ label: 'Milan', value: 'europe-west8', geography: 'Europe' },
|
||||
{ label: 'Paris', value: 'europe-west9', geography: 'Europe' },
|
||||
{ label: 'Berlin', value: 'europe-west10', geography: 'Europe' },
|
||||
{ label: 'Turin', value: 'europe-west12', geography: 'Europe' },
|
||||
{ label: 'Doha', value: 'me-central1', geography: 'Middle East' },
|
||||
{ label: 'Dammam', value: 'me-central2', geography: 'Middle East' },
|
||||
{ label: 'Tel Aviv', value: 'me-west1', geography: 'Middle East' },
|
||||
{
|
||||
label: 'Montréal',
|
||||
value: 'northamerica-northeast1',
|
||||
geography: 'North America',
|
||||
},
|
||||
{
|
||||
label: 'Toronto',
|
||||
value: 'northamerica-northeast2',
|
||||
geography: 'North America',
|
||||
},
|
||||
{
|
||||
label: 'Querétaro',
|
||||
value: 'northamerica-south1',
|
||||
geography: 'North America',
|
||||
},
|
||||
{
|
||||
label: 'São Paulo',
|
||||
value: 'southamerica-east1',
|
||||
geography: 'South America',
|
||||
},
|
||||
{ label: 'Santiago', value: 'southamerica-west1', geography: 'South America' },
|
||||
{ label: 'Council Bluffs', value: 'us-central1', geography: 'North America' },
|
||||
{ label: 'Moncks Corner', value: 'us-east1', geography: 'North America' },
|
||||
{ label: 'Ashburn', value: 'us-east4', geography: 'North America' },
|
||||
{ label: 'Columbus', value: 'us-east5', geography: 'North America' },
|
||||
{ label: 'Dallas', value: 'us-south1', geography: 'North America' },
|
||||
{ label: 'The Dalles', value: 'us-west1', geography: 'North America' },
|
||||
{ label: 'Los Angeles', value: 'us-west2', geography: 'North America' },
|
||||
{ label: 'Salt Lake City', value: 'us-west3', geography: 'North America' },
|
||||
{ label: 'Las Vegas', value: 'us-west4', geography: 'North America' },
|
||||
];
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
export enum IntegrationType {
|
||||
AWS_SERVICES = 'aws',
|
||||
AZURE_SERVICES = 'azure',
|
||||
GCP_SERVICES = 'gcp',
|
||||
}
|
||||
|
||||
interface LogField {
|
||||
@@ -87,7 +88,10 @@ export interface ServiceData {
|
||||
export interface CloudAccount {
|
||||
id: string;
|
||||
cloud_account_id: string;
|
||||
config: AzureCloudAccountConfig | AWSCloudAccountConfig;
|
||||
config:
|
||||
| AzureCloudAccountConfig
|
||||
| AWSCloudAccountConfig
|
||||
| GCPCloudAccountConfig;
|
||||
status: AccountStatus | IServiceStatus;
|
||||
providerAccountId: string;
|
||||
}
|
||||
@@ -97,6 +101,12 @@ export interface AzureCloudAccountConfig {
|
||||
resource_groups: string[];
|
||||
}
|
||||
|
||||
export interface GCPCloudAccountConfig {
|
||||
deployment_region: string;
|
||||
deployment_project_id: string;
|
||||
project_ids: string[];
|
||||
}
|
||||
|
||||
export interface AccountStatus {
|
||||
integration: IntegrationStatus;
|
||||
}
|
||||
@@ -111,6 +121,12 @@ export interface AzureRegion {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface GCPRegion {
|
||||
label: string;
|
||||
geography: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface UpdateServiceConfigPayload {
|
||||
cloud_account_id: string;
|
||||
config: AzureServicesConfig;
|
||||
|
||||
@@ -9,4 +9,6 @@
|
||||
:global([class*='dashboardPageHeader']) {
|
||||
display: none;
|
||||
}
|
||||
// remove margin added by the hidden header to avoid extra whitespace at the top of the page
|
||||
margin-top: calc(var(--spacing-8) * -1);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
--dialog-footer-padding: var(--spacing-8) var(--spacing-12);
|
||||
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
|
||||
// The drawer body — children render inside [data-slot='drawer-description']
|
||||
// (this is the @signozhq drawer, not antd, so .ant-drawer-body was a no-op).
|
||||
@@ -27,8 +27,6 @@
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-12);
|
||||
padding: var(--spacing-10) var(--spacing-12);
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
[data-slot='select-content'] {
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
gap: var(--spacing-5);
|
||||
padding: var(--spacing-6);
|
||||
border-radius: 6px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l2-border);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
.patternBox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-6);
|
||||
padding: var(--spacing-6);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--l2-border);
|
||||
@@ -17,14 +18,14 @@
|
||||
.patternChips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-0) var(--spacing-3);
|
||||
gap: var(--spacing-3);
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
.patternChip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-2);
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
.patternChipRemove {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
padding: var(--spacing-5) var(--spacing-6);
|
||||
border-radius: var(--radius-2);
|
||||
border: 1px solid transparent;
|
||||
background: var(--l3-background);
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
// Include padding + border in the 100% width so the card fits inside
|
||||
@@ -66,11 +67,16 @@
|
||||
|
||||
// Radix RadioGroupItem renders <button data-state="checked|unchecked">.
|
||||
// Use :has() to highlight the wrapper card when its inner button is checked.
|
||||
&:has(button[data-state='checked']) {
|
||||
&.sourceRadioAuto:has(button[data-state='checked']) {
|
||||
background: color-mix(in srgb, var(--accent-primary) 10%, transparent);
|
||||
border-color: color-mix(in srgb, var(--accent-primary) 30%, transparent);
|
||||
}
|
||||
|
||||
&.sourceRadioOverride:has(button[data-state='checked']) {
|
||||
background: color-mix(in srgb, var(--accent-amber) 10%, transparent);
|
||||
border-color: color-mix(in srgb, var(--accent-amber) 30%, transparent);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--l3-background-hover);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ function SourceSelector({
|
||||
>
|
||||
<RadioGroupItem
|
||||
value="auto"
|
||||
containerClassName={styles.sourceRadio}
|
||||
containerClassName={cx(styles.sourceRadio, styles.sourceRadioAuto)}
|
||||
testId="drawer-source-auto"
|
||||
disabled={disableAuto}
|
||||
>
|
||||
@@ -73,7 +73,7 @@ function SourceSelector({
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem
|
||||
value="override"
|
||||
containerClassName={styles.sourceRadio}
|
||||
containerClassName={cx(styles.sourceRadio, styles.sourceRadioOverride)}
|
||||
testId="drawer-source-override"
|
||||
>
|
||||
<div className={styles.sourceRadioTitle}>User override</div>
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
border: 1px solid color-mix(in srgb, var(--bg-amber-400) 30%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-amber-400) 8%, transparent);
|
||||
color: var(--bg-amber-300, var(--bg-amber-400));
|
||||
margin-top: var(--spacing-4);
|
||||
}
|
||||
|
||||
.bannerIcon {
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('LLMObservability (integration)', () => {
|
||||
|
||||
it('renders the overview panel and the tab bar on the overview route', () => {
|
||||
render(<LLMObservability />, undefined, {
|
||||
initialRoute: ROUTES.AI_OBSERVABILITY_OVERVIEW,
|
||||
initialRoute: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('llm-observability-tabs')).toBeInTheDocument();
|
||||
@@ -55,32 +55,32 @@ describe('LLMObservability (integration)', () => {
|
||||
it('navigates to the configuration route when the Model pricing tab is clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<LLMObservability />, undefined, {
|
||||
initialRoute: ROUTES.AI_OBSERVABILITY_OVERVIEW,
|
||||
initialRoute: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('tab', { name: 'Model pricing' }));
|
||||
|
||||
expect(safeNavigateMock).toHaveBeenCalledWith(
|
||||
ROUTES.AI_OBSERVABILITY_CONFIGURATION,
|
||||
ROUTES.LLM_OBSERVABILITY_CONFIGURATION,
|
||||
);
|
||||
});
|
||||
|
||||
it('navigates to the attribute mapping route when that tab is clicked', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
render(<LLMObservability />, undefined, {
|
||||
initialRoute: ROUTES.AI_OBSERVABILITY_OVERVIEW,
|
||||
initialRoute: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('tab', { name: 'Attribute Mapping' }));
|
||||
|
||||
expect(safeNavigateMock).toHaveBeenCalledWith(
|
||||
ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING,
|
||||
ROUTES.LLM_OBSERVABILITY_ATTRIBUTE_MAPPING,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the attribute mapping page on the attribute mapping route', () => {
|
||||
render(<LLMObservability />, undefined, {
|
||||
initialRoute: ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING,
|
||||
initialRoute: ROUTES.LLM_OBSERVABILITY_ATTRIBUTE_MAPPING,
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -91,7 +91,7 @@ describe('LLMObservability (integration)', () => {
|
||||
it('renders the model-pricing page on the configuration route', async () => {
|
||||
setupList();
|
||||
render(<LLMObservability />, undefined, {
|
||||
initialRoute: ROUTES.AI_OBSERVABILITY_CONFIGURATION,
|
||||
initialRoute: ROUTES.LLM_OBSERVABILITY_CONFIGURATION,
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
|
||||
@@ -8,9 +8,9 @@ import LLMObservabilityAttributeMapping from '../AttributeMapping/LLMObservabili
|
||||
import Overview from '../Overview/Overview';
|
||||
import LLMObservabilityModelPricing from '../Settings/ModelPricing/LLMObservabilityModelPricing';
|
||||
|
||||
const OVERVIEW_KEY = ROUTES.AI_OBSERVABILITY_OVERVIEW;
|
||||
const CONFIGURATION_KEY = ROUTES.AI_OBSERVABILITY_CONFIGURATION;
|
||||
const ATTRIBUTE_MAPPING_KEY = ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING;
|
||||
const OVERVIEW_KEY = ROUTES.LLM_OBSERVABILITY_OVERVIEW;
|
||||
const CONFIGURATION_KEY = ROUTES.LLM_OBSERVABILITY_CONFIGURATION;
|
||||
const ATTRIBUTE_MAPPING_KEY = ROUTES.LLM_OBSERVABILITY_ATTRIBUTE_MAPPING;
|
||||
|
||||
interface UseLLMObservabilityTabsResult {
|
||||
items: TabItemProps[];
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
.ant-table-cell {
|
||||
border: 1px solid var(--l1-border);
|
||||
background: var(--l2-background);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.attribute-name {
|
||||
@@ -34,12 +33,12 @@
|
||||
.attribute-pin {
|
||||
cursor: pointer;
|
||||
|
||||
padding: 14px 8px 8px;
|
||||
vertical-align: top;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
|
||||
.log-attribute-pin {
|
||||
padding: 0;
|
||||
padding: 8px;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@@ -15,13 +15,11 @@
|
||||
.ant-tree-node-content-wrapper {
|
||||
user-select: text !important;
|
||||
cursor: text !important;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ant-tree-title {
|
||||
user-select: text !important;
|
||||
cursor: text !important;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5262,24 +5262,6 @@ const onboardingConfigWithLinks = [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
dataSource: 'temporal-cloud-metrics',
|
||||
label: 'Temporal Cloud Metrics',
|
||||
imgUrl: temporalUrl,
|
||||
tags: ['metrics'],
|
||||
module: 'metrics',
|
||||
relatedSearchKeywords: [
|
||||
'metrics',
|
||||
'integrations',
|
||||
'temporal',
|
||||
'temporal cloud',
|
||||
'temporal cloud metrics',
|
||||
'temporal metrics',
|
||||
'openmetrics',
|
||||
'prometheus',
|
||||
],
|
||||
link: '/docs/integrations/temporal-cloud-metrics/',
|
||||
},
|
||||
{
|
||||
dataSource: 'temporal',
|
||||
label: 'Temporal',
|
||||
@@ -5291,6 +5273,9 @@ const onboardingConfigWithLinks = [
|
||||
'application performance monitoring',
|
||||
'integrations',
|
||||
'temporal',
|
||||
'temporal cloud',
|
||||
'temporal logs',
|
||||
'temporal metrics',
|
||||
'temporal traces',
|
||||
'traces',
|
||||
'tracing',
|
||||
@@ -5299,6 +5284,12 @@ const onboardingConfigWithLinks = [
|
||||
desc: 'What are you using ?',
|
||||
type: 'select',
|
||||
options: [
|
||||
{
|
||||
key: 'temporal-cloud',
|
||||
label: 'Cloud Metrics',
|
||||
imgUrl: temporalUrl,
|
||||
link: '/docs/integrations/temporal-cloud-metrics/',
|
||||
},
|
||||
{
|
||||
key: 'temporal-golang',
|
||||
label: 'Go',
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { MetricAggregation } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { ExtendedSelectOption } from 'types/common/select';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import OptionRenderer from '../QueryBuilderSearch/OptionRenderer';
|
||||
@@ -39,7 +39,6 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
||||
signalSource,
|
||||
setAttributeKeys,
|
||||
}: AgregatorFilterProps): JSX.Element {
|
||||
const getPopupContainer = useSelectPopupContainer();
|
||||
const queryClient = useQueryClient();
|
||||
const [optionsData, setOptionsData] = useState<ExtendedSelectOption[]>([]);
|
||||
|
||||
@@ -290,7 +289,7 @@ export const AggregatorFilter = memo(function AggregatorFilter({
|
||||
|
||||
return (
|
||||
<AutoComplete
|
||||
getPopupContainer={getPopupContainer}
|
||||
getPopupContainer={popupContainer}
|
||||
placeholder={getPlaceholder()}
|
||||
style={selectStyle}
|
||||
filterOption={false}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Select, SelectProps, Space } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { getCategorySelectOptionByName } from 'container/NewWidget/RightContainer/alertFomatCategories';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { categoryToSupport } from './config';
|
||||
import { selectStyles } from './styles';
|
||||
@@ -13,7 +13,6 @@ function BuilderUnitsFilter({
|
||||
onChange,
|
||||
yAxisUnit,
|
||||
}: IBuilderUnitsFilterProps): JSX.Element {
|
||||
const getPopupContainer = useSelectPopupContainer();
|
||||
const { currentQuery, handleOnUnitsChange } = useQueryBuilder();
|
||||
|
||||
const selectedValue = yAxisUnit || currentQuery?.unit;
|
||||
@@ -37,7 +36,7 @@ function BuilderUnitsFilter({
|
||||
Y-axis unit
|
||||
</Typography.Text>
|
||||
<Select
|
||||
getPopupContainer={getPopupContainer}
|
||||
getPopupContainer={popupContainer}
|
||||
style={selectStyles}
|
||||
onChange={onChangeHandler}
|
||||
value={selectedValue}
|
||||
|
||||
@@ -9,13 +9,12 @@ import {
|
||||
} from 'lib/query/transformQueryBuilderData';
|
||||
import { Having, HavingForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { getHavingObject, isValidHavingValue } from '../../utils';
|
||||
import { HavingFilterProps, HavingTagRenderProps } from './types';
|
||||
|
||||
function HavingFilter({ formula, onChange }: HavingFilterProps): JSX.Element {
|
||||
const getPopupContainer = useSelectPopupContainer();
|
||||
const { having } = formula;
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [localValues, setLocalValues] = useState<string[]>([]);
|
||||
@@ -172,7 +171,7 @@ function HavingFilter({ formula, onChange }: HavingFilterProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<Select
|
||||
getPopupContainer={getPopupContainer}
|
||||
getPopupContainer={popupContainer}
|
||||
autoClearSearchValue={false}
|
||||
mode="multiple"
|
||||
onSearch={handleSearch}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react';
|
||||
import { Select, Spin } from 'antd';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { selectStyle } from '../../QueryBuilderSearch/config';
|
||||
import { OrderByProps } from './types';
|
||||
@@ -13,7 +13,6 @@ function OrderByFilter({
|
||||
onChange,
|
||||
query,
|
||||
}: OrderByProps): JSX.Element {
|
||||
const getPopupContainer = useSelectPopupContainer();
|
||||
const {
|
||||
debouncedSearchText,
|
||||
createOptions,
|
||||
@@ -65,7 +64,7 @@ function OrderByFilter({
|
||||
|
||||
return (
|
||||
<Select
|
||||
getPopupContainer={getPopupContainer}
|
||||
getPopupContainer={popupContainer}
|
||||
mode="tags"
|
||||
style={selectStyle}
|
||||
onSearch={handleSearchKeys}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { isEqual, uniqWith } from 'lodash-es';
|
||||
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import OptionRenderer from '../QueryBuilderSearch/OptionRenderer';
|
||||
@@ -33,7 +33,6 @@ export const GroupByFilter = memo(function GroupByFilter({
|
||||
disabled,
|
||||
signalSource,
|
||||
}: GroupByFilterProps): JSX.Element {
|
||||
const getPopupContainer = useSelectPopupContainer();
|
||||
const queryClient = useQueryClient();
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [optionsData, setOptionsData] = useState<
|
||||
@@ -175,7 +174,7 @@ export const GroupByFilter = memo(function GroupByFilter({
|
||||
|
||||
return (
|
||||
<Select
|
||||
getPopupContainer={getPopupContainer}
|
||||
getPopupContainer={popupContainer}
|
||||
mode="tags"
|
||||
style={selectStyle}
|
||||
onSearch={handleSearchKeys}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { Having, HavingForm } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { SelectOption } from 'types/common/select';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { getHavingObject, isValidHavingValue } from '../utils';
|
||||
// ** Types
|
||||
@@ -27,7 +27,6 @@ export function HavingFilter({
|
||||
query,
|
||||
onChange,
|
||||
}: HavingFilterProps): JSX.Element {
|
||||
const getPopupContainer = useSelectPopupContainer();
|
||||
const { having } = query;
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
const [options, setOptions] = useState<SelectOption<string, string>[]>([]);
|
||||
@@ -232,7 +231,7 @@ export function HavingFilter({
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
getPopupContainer={getPopupContainer}
|
||||
getPopupContainer={popupContainer}
|
||||
autoClearSearchValue={false}
|
||||
mode="multiple"
|
||||
onSearch={handleSearch}
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { MetricAggregation } from 'types/api/v5/queryRange';
|
||||
import { ExtendedSelectOption } from 'types/common/select';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import OptionRenderer from '../QueryBuilderSearch/OptionRenderer';
|
||||
@@ -85,7 +85,6 @@ export const MetricNameSelector = memo(function MetricNameSelector({
|
||||
signalSource,
|
||||
'data-testid': dataTestId,
|
||||
}: MetricNameSelectorProps): JSX.Element {
|
||||
const getPopupContainer = useSelectPopupContainer();
|
||||
const currentMetricName =
|
||||
(query.aggregations?.[0] as MetricAggregation)?.metricName ||
|
||||
query.aggregateAttribute?.key ||
|
||||
@@ -273,7 +272,7 @@ export const MetricNameSelector = memo(function MetricNameSelector({
|
||||
return (
|
||||
<AutoComplete
|
||||
className="metric-name-selector"
|
||||
getPopupContainer={getPopupContainer}
|
||||
getPopupContainer={popupContainer}
|
||||
style={selectStyle}
|
||||
filterOption={false}
|
||||
placeholder={placeholder}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Select, Spin } from 'antd';
|
||||
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
|
||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
import { getParsedAggregationOptionsForOrderBy } from 'utils/aggregationConverter';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
import { OrderByFilterProps } from './OrderByFilter.interfaces';
|
||||
@@ -16,7 +16,6 @@ export function OrderByFilter({
|
||||
entityVersion,
|
||||
isNewQueryV2 = false,
|
||||
}: OrderByFilterProps): JSX.Element {
|
||||
const getPopupContainer = useSelectPopupContainer();
|
||||
const {
|
||||
debouncedSearchText,
|
||||
selectedValue,
|
||||
@@ -79,7 +78,7 @@ export function OrderByFilter({
|
||||
|
||||
return (
|
||||
<Select
|
||||
getPopupContainer={getPopupContainer}
|
||||
getPopupContainer={popupContainer}
|
||||
mode="tags"
|
||||
style={selectStyle}
|
||||
onSearch={handleSearchKeys}
|
||||
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { FeatureKeys } from '../../../../constants/features';
|
||||
@@ -95,7 +95,6 @@ function QueryBuilderSearch({
|
||||
disableNavigationShortcuts,
|
||||
entity,
|
||||
}: QueryBuilderSearchProps): JSX.Element {
|
||||
const getPopupContainer = useSelectPopupContainer();
|
||||
const { pathname } = useLocation();
|
||||
const isLogsExplorerPage = useMemo(
|
||||
() => pathname === ROUTES.LOGS_EXPLORER,
|
||||
@@ -398,7 +397,7 @@ function QueryBuilderSearch({
|
||||
<Select
|
||||
data-testid={'qb-search-select'}
|
||||
ref={selectRef}
|
||||
getPopupContainer={getPopupContainer}
|
||||
getPopupContainer={popupContainer}
|
||||
transitionName=""
|
||||
choiceTransitionName=""
|
||||
virtual={false}
|
||||
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
TagFilter,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { selectStyle } from '../QueryBuilderSearch/config';
|
||||
@@ -157,8 +157,6 @@ function QueryBuilderSearchV2(
|
||||
selectProps,
|
||||
} = props;
|
||||
|
||||
const getPopupContainer = useSelectPopupContainer();
|
||||
|
||||
const { registerShortcut, deregisterShortcut } = useKeyboardHotkeys();
|
||||
|
||||
const { handleRunQuery, currentQuery } = useQueryBuilder();
|
||||
@@ -991,7 +989,7 @@ function QueryBuilderSearchV2(
|
||||
{...selectProps}
|
||||
data-testid={'qb-search-select'}
|
||||
ref={selectRef}
|
||||
{...(hasPopupContainer ? { getPopupContainer } : {})}
|
||||
{...(hasPopupContainer ? { getPopupContainer: popupContainer } : {})}
|
||||
{...(maxTagCount ? { maxTagCount } : {})}
|
||||
key={queryTags.join('.')}
|
||||
virtual={false}
|
||||
|
||||
@@ -294,7 +294,7 @@ function SideNav({ isPinned }: { isPinned: boolean }): JSX.Element {
|
||||
if (item.key === ROUTES.INTEGRATIONS) {
|
||||
return shouldShowIntegrationsValue;
|
||||
}
|
||||
if (item.key === ROUTES.AI_OBSERVABILITY_OVERVIEW) {
|
||||
if (item.key === ROUTES.LLM_OBSERVABILITY_OVERVIEW) {
|
||||
return isAIObservabilityEnabled;
|
||||
}
|
||||
return item.isEnabled;
|
||||
|
||||
@@ -290,14 +290,14 @@ export const defaultMoreMenuItems: SidebarItem[] = [
|
||||
itemKey: 'external-apis',
|
||||
},
|
||||
{
|
||||
key: ROUTES.AI_OBSERVABILITY_OVERVIEW,
|
||||
label: 'AI Observability',
|
||||
key: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
|
||||
label: 'LLM Observability',
|
||||
icon: <Brain size={16} />,
|
||||
isNew: true,
|
||||
// Gated behind the `enable_ai_observability` feature flag in
|
||||
// SideNav's `computedSecondaryMenuItems`; disabled by default.
|
||||
isEnabled: false,
|
||||
itemKey: 'ai-observability',
|
||||
itemKey: 'llm-observability',
|
||||
},
|
||||
{
|
||||
key: ROUTES.METER,
|
||||
@@ -576,7 +576,7 @@ export const NEW_ROUTES_MENU_ITEM_KEY_MAP: Record<string, string> = {
|
||||
ROUTES.INFRASTRUCTURE_MONITORING_HOSTS,
|
||||
[ROUTES.API_MONITORING_BASE]: ROUTES.API_MONITORING,
|
||||
[ROUTES.MESSAGING_QUEUES_BASE]: ROUTES.MESSAGING_QUEUES_OVERVIEW,
|
||||
[ROUTES.AI_OBSERVABILITY_BASE]: ROUTES.AI_OBSERVABILITY_OVERVIEW,
|
||||
[ROUTES.LLM_OBSERVABILITY_BASE]: ROUTES.LLM_OBSERVABILITY_OVERVIEW,
|
||||
// `getActiveMenuKeyFromPath` strips the URL down to its first segment;
|
||||
// `/ai-assistant/<id>` reduces to `/ai-assistant`, which we point back
|
||||
// to the AI Assistant menu item's concrete key.
|
||||
|
||||
@@ -203,9 +203,9 @@ export const routesToSkip = [
|
||||
ROUTES.METER_EXPLORER_VIEWS,
|
||||
ROUTES.METRICS_EXPLORER_VOLUME_CONTROL,
|
||||
ROUTES.SOMETHING_WENT_WRONG,
|
||||
ROUTES.AI_OBSERVABILITY_OVERVIEW,
|
||||
ROUTES.AI_OBSERVABILITY_CONFIGURATION,
|
||||
ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING,
|
||||
ROUTES.LLM_OBSERVABILITY_OVERVIEW,
|
||||
ROUTES.LLM_OBSERVABILITY_CONFIGURATION,
|
||||
ROUTES.LLM_OBSERVABILITY_ATTRIBUTE_MAPPING,
|
||||
];
|
||||
|
||||
export const routesToDisable = [ROUTES.LOGS_EXPLORER, ROUTES.LIVE_LOGS];
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
export function isOneClickIntegration(integrationId: string): boolean {
|
||||
return (
|
||||
integrationId === INTEGRATION_TYPES.AWS ||
|
||||
integrationId === INTEGRATION_TYPES.AZURE
|
||||
integrationId === INTEGRATION_TYPES.AZURE ||
|
||||
integrationId === INTEGRATION_TYPES.GCP
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,12 @@ export function useAccountSettingsModal({
|
||||
}: UseAccountSettingsModalProps): UseAccountSettingsModal {
|
||||
const [form] = Form.useForm();
|
||||
const { mutate: updateAccount, isLoading } = useUpdateAccount();
|
||||
// `account.config` is the shared per-provider union (Azure | AWS | GCP).
|
||||
// Narrow to Azure by `resource_groups` (Azure-only) rather than
|
||||
// `deployment_region`, which GCP also has — so it no longer identifies
|
||||
// Azure uniquely.
|
||||
const accountConfig = useMemo(
|
||||
() => ('deployment_region' in account.config ? account.config : null),
|
||||
() => ('resource_groups' in account.config ? account.config : null),
|
||||
[account.config],
|
||||
);
|
||||
const [resourceGroups, setResourceGroups] = useState<string[]>(
|
||||
|
||||
148
frontend/src/hooks/integration/gcp/useAccountSettingsDrawer.ts
Normal file
148
frontend/src/hooks/integration/gcp/useAccountSettingsDrawer.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import {
|
||||
Dispatch,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { Form } from 'antd';
|
||||
import { FormInstance } from 'antd/lib';
|
||||
import { useUpdateAccount } from 'api/generated/services/cloudintegration';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { CloudAccount } from 'container/Integrations/types';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
|
||||
interface UseAccountSettingsDrawerProps {
|
||||
onClose: () => void;
|
||||
account: CloudAccount;
|
||||
setActiveAccount: Dispatch<SetStateAction<CloudAccount | null>>;
|
||||
}
|
||||
|
||||
interface UseAccountSettingsDrawer {
|
||||
form: FormInstance;
|
||||
isLoading: boolean;
|
||||
projectIds: string[];
|
||||
isSaveDisabled: boolean;
|
||||
setProjectIds: Dispatch<SetStateAction<string[]>>;
|
||||
handleSubmit: () => Promise<void>;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
export function useAccountSettingsDrawer({
|
||||
onClose,
|
||||
account,
|
||||
setActiveAccount,
|
||||
}: UseAccountSettingsDrawerProps): UseAccountSettingsDrawer {
|
||||
const [form] = Form.useForm();
|
||||
const { mutate: updateAccount, isLoading } = useUpdateAccount();
|
||||
const accountConfig = useMemo(
|
||||
() => ('project_ids' in account.config ? account.config : null),
|
||||
[account.config],
|
||||
);
|
||||
const [projectIds, setProjectIds] = useState<string[]>(
|
||||
accountConfig?.project_ids || [],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
projectIds: accountConfig.project_ids,
|
||||
});
|
||||
setProjectIds(accountConfig.project_ids);
|
||||
}, [accountConfig, form]);
|
||||
|
||||
const handleSubmit = useCallback(async (): Promise<void> => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
if (!accountConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateAccount(
|
||||
{
|
||||
pathParams: {
|
||||
cloudProvider: INTEGRATION_TYPES.GCP,
|
||||
id: account?.id || '',
|
||||
},
|
||||
data: {
|
||||
config: {
|
||||
gcp: {
|
||||
// Deployment region & project ID are immutable in the UI, but the
|
||||
// Updatable GCP DTO requires all three fields to be sent.
|
||||
deploymentRegion: accountConfig.deployment_region,
|
||||
deploymentProjectId: accountConfig.deployment_project_id,
|
||||
projectIds: values.projectIds || [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
const nextConfig = {
|
||||
deployment_region: accountConfig.deployment_region,
|
||||
deployment_project_id: accountConfig.deployment_project_id,
|
||||
project_ids: values.projectIds || [],
|
||||
};
|
||||
|
||||
setActiveAccount({
|
||||
...account,
|
||||
config: nextConfig,
|
||||
});
|
||||
onClose();
|
||||
|
||||
toast.success('Account settings updated successfully', {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
|
||||
void logEvent('GCP Integration: Account settings updated', {
|
||||
cloudAccountId: account.cloud_account_id,
|
||||
deploymentRegion: nextConfig.deployment_region,
|
||||
projectIds: nextConfig.project_ids,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error('Failed to update account settings', {
|
||||
description: error?.message,
|
||||
position: 'bottom-right',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Form submission failed:', error);
|
||||
}
|
||||
}, [form, updateAccount, account, accountConfig, setActiveAccount, onClose]);
|
||||
|
||||
const isSaveDisabled = useMemo(() => {
|
||||
if (!accountConfig) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isEqual(
|
||||
[...(projectIds || [])].sort(),
|
||||
[...accountConfig.project_ids].sort(),
|
||||
);
|
||||
}, [accountConfig, projectIds]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
return {
|
||||
form,
|
||||
isLoading,
|
||||
projectIds,
|
||||
isSaveDisabled,
|
||||
setProjectIds,
|
||||
handleSubmit,
|
||||
handleClose,
|
||||
};
|
||||
}
|
||||
165
frontend/src/hooks/integration/gcp/useCloudAccountSetupDrawer.ts
Normal file
165
frontend/src/hooks/integration/gcp/useCloudAccountSetupDrawer.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import {
|
||||
CreateAccountMutationResult,
|
||||
GetConnectionCredentialsQueryResult,
|
||||
invalidateListAccounts,
|
||||
useAgentCheckIn,
|
||||
useCreateAccount,
|
||||
useGetConnectionCredentials,
|
||||
} from 'api/generated/services/cloudintegration';
|
||||
import {
|
||||
CloudintegrationtypesCredentialsDTO,
|
||||
CloudintegrationtypesPostableAccountDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { ErrorType } from 'api/generatedAPIInstance';
|
||||
import { INTEGRATION_TYPES } from 'container/Integrations/constants';
|
||||
import { GcpSetupFormValues } from 'container/Integrations/CloudIntegration/GoogleCloudPlatform/AddNewAccount/types';
|
||||
import useAxiosError from 'hooks/useAxiosError';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import logEvent from '../../../api/common/logEvent';
|
||||
|
||||
interface UseCloudAccountSetupDrawerProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface UseCloudAccountSetupDrawer {
|
||||
isLoading: boolean;
|
||||
connectAccount: (values: GcpSetupFormValues) => Promise<void>;
|
||||
handleClose: () => void;
|
||||
connectionParams?: CloudintegrationtypesCredentialsDTO;
|
||||
isConnectionParamsLoading: boolean;
|
||||
submitError: string | null;
|
||||
clearSubmitError: () => void;
|
||||
}
|
||||
|
||||
export function useCloudAccountSetupDrawer({
|
||||
onClose,
|
||||
}: UseCloudAccountSetupDrawerProps): UseCloudAccountSetupDrawer {
|
||||
const queryClient = useQueryClient();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||
|
||||
const clearSubmitError = useCallback((): void => {
|
||||
setSubmitError(null);
|
||||
}, []);
|
||||
|
||||
const { mutateAsync: createAccount } = useCreateAccount();
|
||||
const { mutateAsync: checkIn } = useAgentCheckIn();
|
||||
const handleError = useAxiosError();
|
||||
|
||||
const { data: connectionParams, isLoading: isConnectionParamsLoading } =
|
||||
useGetConnectionCredentials<GetConnectionCredentialsQueryResult>(
|
||||
{
|
||||
cloudProvider: INTEGRATION_TYPES.GCP,
|
||||
},
|
||||
{
|
||||
query: {
|
||||
onError: handleError,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
onClose();
|
||||
}, [onClose]);
|
||||
|
||||
const handleConnectionSuccess = useCallback(
|
||||
(payload: {
|
||||
cloudIntegrationId: string;
|
||||
providerAccountId: string;
|
||||
}): void => {
|
||||
void logEvent('GCP Integration: Account connected', {
|
||||
cloudIntegrationId: payload.cloudIntegrationId,
|
||||
providerAccountId: payload.providerAccountId,
|
||||
});
|
||||
toast.success('GCP account connected successfully', {
|
||||
position: 'bottom-right',
|
||||
});
|
||||
void invalidateListAccounts(queryClient, {
|
||||
cloudProvider: INTEGRATION_TYPES.GCP,
|
||||
});
|
||||
handleClose();
|
||||
},
|
||||
[handleClose, queryClient],
|
||||
);
|
||||
|
||||
const connectAccount = useCallback(
|
||||
async (values: GcpSetupFormValues): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setSubmitError(null);
|
||||
|
||||
const payload: CloudintegrationtypesPostableAccountDTO = {
|
||||
config: {
|
||||
gcp: {
|
||||
deploymentRegion: values.deploymentRegion,
|
||||
deploymentProjectId: values.deploymentProjectId,
|
||||
projectIds: values.projectIds || [],
|
||||
},
|
||||
},
|
||||
credentials: {
|
||||
// Cloud users can't edit these — the backend-provided credentials are
|
||||
// authoritative. Enterprise users have no backend defaults and enter
|
||||
// their own (validated non-empty), so their form values are used.
|
||||
ingestionUrl: connectionParams?.data?.ingestionUrl || values.ingestionUrl,
|
||||
ingestionKey: connectionParams?.data?.ingestionKey || values.ingestionKey,
|
||||
sigNozApiUrl: connectionParams?.data?.sigNozApiUrl || values.sigNozApiUrl,
|
||||
sigNozApiKey: connectionParams?.data?.sigNozApiKey || values.sigNozApiKey,
|
||||
},
|
||||
};
|
||||
|
||||
// Step 1: create the integration account.
|
||||
const createResponse: CreateAccountMutationResult = await createAccount({
|
||||
pathParams: { cloudProvider: INTEGRATION_TYPES.GCP },
|
||||
data: payload,
|
||||
});
|
||||
|
||||
const cloudIntegrationId = createResponse.data.id;
|
||||
const providerAccountId = values.accountName;
|
||||
|
||||
void logEvent('GCP Integration: Account created', {
|
||||
id: cloudIntegrationId,
|
||||
});
|
||||
|
||||
// Step 2: mimic the agent by checking in from the frontend (manual flow).
|
||||
await checkIn({
|
||||
pathParams: { cloudProvider: INTEGRATION_TYPES.GCP },
|
||||
data: {
|
||||
providerAccountId,
|
||||
cloudIntegrationId,
|
||||
data: {},
|
||||
},
|
||||
});
|
||||
|
||||
handleConnectionSuccess({ cloudIntegrationId, providerAccountId });
|
||||
} catch (error) {
|
||||
// Surface the backend's message inline in the drawer instead of a
|
||||
// generic failure string.
|
||||
const message = toAPIError(
|
||||
error as ErrorType<RenderErrorResponseDTO>,
|
||||
'Failed to connect GCP account',
|
||||
).getErrorMessage();
|
||||
setSubmitError(message);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[connectionParams, createAccount, checkIn, handleConnectionSuccess],
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
connectAccount,
|
||||
handleClose,
|
||||
connectionParams: connectionParams?.data as
|
||||
| CloudintegrationtypesCredentialsDTO
|
||||
| undefined,
|
||||
isConnectionParamsLoading,
|
||||
submitError,
|
||||
clearSubmitError,
|
||||
};
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import AppRoutes from 'AppRoutes';
|
||||
import { AxiosError } from 'axios';
|
||||
import { GlobalTimeStoreAdapter } from 'components/GlobalTimeStoreAdapter/GlobalTimeStoreAdapter';
|
||||
import { ThemeProvider } from 'hooks/useDarkMode';
|
||||
import { configureOverlayScrollbars } from 'lib/configureOverlayScrollbars';
|
||||
import { NuqsAdapter } from 'nuqs/adapters/react';
|
||||
import { AppProvider } from 'providers/App/App';
|
||||
import TimezoneProvider from 'providers/Timezone';
|
||||
@@ -18,8 +17,6 @@ import './ReactI18';
|
||||
|
||||
import 'styles.scss';
|
||||
|
||||
configureOverlayScrollbars();
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { OverlayScrollbars } from 'overlayscrollbars';
|
||||
|
||||
/**
|
||||
* Disables the content `elementEvents` option (default: `[['img', 'load']]`)
|
||||
* for every OverlayScrollbars instance.
|
||||
*
|
||||
* The per-element event cleanups OverlayScrollbars stores in its internal
|
||||
* WeakMap close over the MutationObserver callback scope, which holds arrays
|
||||
* with every node added/removed in that mutation batch. A single long-lived
|
||||
* reference to one of those elements (e.g. CodeMirror's EditContext or its
|
||||
* module-level scratch Range pinning a detached editor) then retains entire
|
||||
* unmounted subtrees as detached DOM — ~1.3k nodes per InfraMonitoring
|
||||
* category switch.
|
||||
*
|
||||
* Content size changes from loading images are still handled by the
|
||||
* scrollbars' size observer, so scrollbar geometry stays correct.
|
||||
*/
|
||||
export function configureOverlayScrollbars(): void {
|
||||
OverlayScrollbars.env().setDefaultOptions({
|
||||
update: { elementEvents: null },
|
||||
});
|
||||
}
|
||||
@@ -126,15 +126,6 @@ export default function UPlotChart({
|
||||
}
|
||||
}, [isDataEmpty, destroyPlot]);
|
||||
|
||||
/**
|
||||
* Destroy the plot on unmount. Without this, uPlot's window-level
|
||||
* `dppxchange` listener keeps the instance (and its whole detached DOM
|
||||
* subtree) alive after the component is gone.
|
||||
*/
|
||||
const destroyPlotRef = useRef(destroyPlot);
|
||||
destroyPlotRef.current = destroyPlot;
|
||||
useEffect(() => (): void => destroyPlotRef.current(), []);
|
||||
|
||||
/**
|
||||
* Handle initialization and prop changes
|
||||
*/
|
||||
|
||||
@@ -327,32 +327,6 @@ describe('UPlotChart', () => {
|
||||
expect(firstInstance.destroy).toHaveBeenCalled();
|
||||
expect(instances).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('destroys the instance and notifies callbacks on unmount', () => {
|
||||
const plotRef = jest.fn();
|
||||
const onDestroy = jest.fn();
|
||||
|
||||
const { unmount } = render(
|
||||
<UPlotChart
|
||||
config={createMockConfig()}
|
||||
data={validData}
|
||||
width={600}
|
||||
height={400}
|
||||
plotRef={plotRef}
|
||||
onDestroy={onDestroy}
|
||||
/>,
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
const firstInstance = instances[0];
|
||||
plotRef.mockClear();
|
||||
|
||||
unmount();
|
||||
|
||||
expect(onDestroy).toHaveBeenCalledWith(firstInstance);
|
||||
expect(firstInstance.destroy).toHaveBeenCalledTimes(1);
|
||||
expect(plotRef).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('spanGaps data transformation', () => {
|
||||
|
||||
@@ -23,14 +23,12 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { DropdownMenuSimple } from '@signozhq/ui/dropdown-menu';
|
||||
import type { MenuItem } from '@signozhq/ui/dropdown-menu';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { cloneDashboardV2 } from 'api/generated/services/dashboard';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useDeleteDashboard } from 'hooks/dashboard/useDeleteDashboard';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import history from 'lib/history';
|
||||
import { DashboardDetailEvents } from 'pages/DashboardPageV2/constants/events';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
@@ -71,7 +69,6 @@ function DashboardActions({
|
||||
}: DashboardActionsProps): JSX.Element {
|
||||
const canEditDashboard = useDashboardStore((s) => s.canEditDashboard);
|
||||
const isLocked = useDashboardStore((s) => s.isLocked);
|
||||
const isEditable = useDashboardStore((s) => s.isEditable);
|
||||
const settingsRequest = useDashboardStore((s) => s.settingsRequest);
|
||||
const clearSettingsRequest = useDashboardStore((s) => s.clearSettingsRequest);
|
||||
const { user } = useAppContext();
|
||||
@@ -115,11 +112,6 @@ function DashboardActions({
|
||||
setIsCloning(true);
|
||||
const response = await cloneDashboardV2({ id: dashboard.id });
|
||||
toast.success('Dashboard cloned');
|
||||
void logEvent(DashboardDetailEvents.Cloned, {
|
||||
dashboardId: dashboard.id,
|
||||
dashboardName: title,
|
||||
source: 'detail',
|
||||
});
|
||||
safeNavigate(
|
||||
generatePath(ROUTES.DASHBOARD, { dashboardId: response.data.id }),
|
||||
);
|
||||
@@ -128,43 +120,16 @@ function DashboardActions({
|
||||
} finally {
|
||||
setIsCloning(false);
|
||||
}
|
||||
}, [dashboard.id, title, safeNavigate, showErrorModal]);
|
||||
}, [dashboard.id, safeNavigate, showErrorModal]);
|
||||
|
||||
const handleConfirmDelete = useCallback((): void => {
|
||||
deleteDashboardMutation.mutate(undefined, {
|
||||
onSuccess: () => {
|
||||
void logEvent(DashboardDetailEvents.Deleted, {
|
||||
dashboardId: dashboard.id,
|
||||
panelCount: Object.keys(dashboard.spec.panels).length,
|
||||
});
|
||||
setIsDeleteOpen(false);
|
||||
history.replace(ROUTES.ALL_DASHBOARD);
|
||||
},
|
||||
});
|
||||
}, [deleteDashboardMutation, dashboard.id, dashboard.spec.panels]);
|
||||
|
||||
const handleOpenSettings = useCallback((): void => {
|
||||
void logEvent(DashboardDetailEvents.SettingsOpened, {
|
||||
dashboardId: dashboard.id,
|
||||
});
|
||||
setIsSettingsDrawerOpen(true);
|
||||
}, [dashboard.id]);
|
||||
|
||||
const handleOpenJsonEditor = useCallback((): void => {
|
||||
void logEvent(DashboardDetailEvents.JsonEditorOpened, {
|
||||
dashboardId: dashboard.id,
|
||||
readOnly: !isEditable,
|
||||
});
|
||||
setIsJsonEditorOpen(true);
|
||||
}, [dashboard.id, isEditable]);
|
||||
|
||||
const handleEnterFullScreen = useCallback((): void => {
|
||||
void logEvent(DashboardDetailEvents.FullScreenToggled, {
|
||||
dashboardId: dashboard.id,
|
||||
enabled: true,
|
||||
});
|
||||
void handle.enter();
|
||||
}, [dashboard.id, handle]);
|
||||
}, [deleteDashboardMutation]);
|
||||
|
||||
// Shown only to edit-permitted users, so the only disabled reason is the lock.
|
||||
const editLabel = useCallback(
|
||||
@@ -211,7 +176,7 @@ function DashboardActions({
|
||||
key: 'fullscreen',
|
||||
label: 'Full screen',
|
||||
icon: <Fullscreen size={14} />,
|
||||
onClick: handleEnterFullScreen,
|
||||
onClick: handle.enter,
|
||||
});
|
||||
|
||||
const items: MenuItem[] = [
|
||||
@@ -263,7 +228,7 @@ function DashboardActions({
|
||||
onOpenRename,
|
||||
handleClone,
|
||||
onLockToggle,
|
||||
handleEnterFullScreen,
|
||||
handle.enter,
|
||||
]);
|
||||
|
||||
return (
|
||||
@@ -293,7 +258,7 @@ function DashboardActions({
|
||||
prefix={<Configure size="md" />}
|
||||
testId="show-drawer"
|
||||
disabled={isLocked}
|
||||
onClick={handleOpenSettings}
|
||||
onClick={(): void => setIsSettingsDrawerOpen(true)}
|
||||
size="md"
|
||||
>
|
||||
Configure
|
||||
@@ -318,7 +283,7 @@ function DashboardActions({
|
||||
className={styles.toolbarButton}
|
||||
prefix={<Braces size="md" />}
|
||||
testId="edit-json"
|
||||
onClick={handleOpenJsonEditor}
|
||||
onClick={(): void => setIsJsonEditorOpen(true)}
|
||||
size="md"
|
||||
>
|
||||
JSON
|
||||
|
||||
@@ -12,10 +12,8 @@ import { Button } from '@signozhq/ui/button';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import cx from 'classnames';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { DashboardDetailEvents } from 'pages/DashboardPageV2/constants/events';
|
||||
import { linkifyText } from 'utils/linkifyText';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
@@ -63,7 +61,6 @@ function DashboardInfo({
|
||||
onCancel,
|
||||
}: DashboardInfoProps): JSX.Element {
|
||||
const canEdit = useDashboardStore((s) => s.isEditable);
|
||||
const dashboardId = useDashboardStore((s) => s.dashboardId);
|
||||
|
||||
const hasTags = tags.length > 0;
|
||||
const hasDescription = !isEmpty(description);
|
||||
@@ -101,14 +98,6 @@ function DashboardInfo({
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenPublicUrl = (): void => {
|
||||
void logEvent(DashboardDetailEvents.PublicUrlOpened, {
|
||||
dashboardId,
|
||||
dashboardName: title,
|
||||
});
|
||||
openInNewTab(publicUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.dashboardInfo}>
|
||||
<img src={image} alt={title} className={styles.dashboardImage} />
|
||||
@@ -193,7 +182,7 @@ function DashboardInfo({
|
||||
className={styles.publicLink}
|
||||
aria-label="Open public dashboard"
|
||||
testId="dashboard-public-link"
|
||||
onClick={handleOpenPublicUrl}
|
||||
onClick={(): void => openInNewTab(publicUrl)}
|
||||
>
|
||||
<Globe size={14} />
|
||||
</Button>
|
||||
|
||||
@@ -6,9 +6,7 @@ import { TooltipSimple } from '@signozhq/ui/tooltip';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { Drawer } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import type { DashboardtypesGettableDashboardV2DTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { DashboardDetailEvents } from 'pages/DashboardPageV2/constants/events';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
|
||||
@@ -53,17 +51,9 @@ function JsonEditorDrawer({
|
||||
const onCopy = useCallback((): void => {
|
||||
copyToClipboard(draft);
|
||||
toast.success('JSON copied to clipboard');
|
||||
void logEvent(DashboardDetailEvents.JsonEditorAction, {
|
||||
action: 'copy',
|
||||
dashboardId: dashboard.id,
|
||||
});
|
||||
}, [copyToClipboard, draft, dashboard.id]);
|
||||
}, [copyToClipboard, draft]);
|
||||
|
||||
const onDownload = useCallback((): void => {
|
||||
void logEvent(DashboardDetailEvents.JsonEditorAction, {
|
||||
action: 'download',
|
||||
dashboardId: dashboard.id,
|
||||
});
|
||||
const blob = new Blob([draft], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
@@ -73,7 +63,7 @@ function JsonEditorDrawer({
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
}, [draft, dashboard.name, dashboard.id]);
|
||||
}, [draft, dashboard.name]);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLDivElement>): void => {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { updateDashboardV2 } from 'api/generated/services/dashboard';
|
||||
import { DashboardDetailEvents } from 'pages/DashboardPageV2/constants/events';
|
||||
import type {
|
||||
DashboardtypesDashboardSpecDTO,
|
||||
DashboardtypesGettableDashboardV2DTO,
|
||||
@@ -142,22 +140,14 @@ export function useJsonEditor({
|
||||
const format = useCallback((): void => {
|
||||
try {
|
||||
setDraft(JSON.stringify(JSON.parse(draft), null, 2));
|
||||
void logEvent(DashboardDetailEvents.JsonEditorAction, {
|
||||
action: 'format',
|
||||
dashboardId,
|
||||
});
|
||||
} catch {
|
||||
// Leave the draft untouched when it can't be parsed.
|
||||
}
|
||||
}, [draft, dashboardId]);
|
||||
}, [draft]);
|
||||
|
||||
const reset = useCallback((): void => {
|
||||
setDraft(appliedText);
|
||||
void logEvent(DashboardDetailEvents.JsonEditorAction, {
|
||||
action: 'reset',
|
||||
dashboardId,
|
||||
});
|
||||
}, [appliedText, dashboardId]);
|
||||
}, [appliedText]);
|
||||
|
||||
const apply = useCallback(async (): Promise<void> => {
|
||||
if (readOnly || !validity.valid || !isDirty) {
|
||||
@@ -173,10 +163,6 @@ export function useJsonEditor({
|
||||
dashboardToUpdatable({ ...dashboard, ...edited }),
|
||||
);
|
||||
toast.success('Dashboard updated');
|
||||
void logEvent(DashboardDetailEvents.JsonEditorAction, {
|
||||
action: 'apply',
|
||||
dashboardId,
|
||||
});
|
||||
refetch();
|
||||
onApplied();
|
||||
} catch (error) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user