Compare commits

..

7 Commits

Author SHA1 Message Date
Abhi Kumar
053d306e99 chore: pr review fixes 2026-07-05 01:58:57 +05:30
Abhi Kumar
6693bb7a06 chore: removed duplicate tests 2026-07-05 01:58:57 +05:30
Abhi Kumar
8f4c00849c chore: fixed lint issue 2026-07-05 01:58:57 +05:30
Abhi Kumar
77390d5a5e chore: cleaned up duplicate tests 2026-07-05 01:58:57 +05:30
Abhi Kumar
cc94b87357 chore: ran oxfmt 2026-07-05 01:58:57 +05:30
Abhi Kumar
adb60c4683 chore: added more tests 2026-07-05 01:58:57 +05:30
Abhi Kumar
7d8f896a5f test: added e2e tests for dashboard create flow 2026-07-05 01:58:56 +05:30
316 changed files with 3493 additions and 14951 deletions

View File

@@ -515,13 +515,6 @@ components:
url:
type: string
type: object
AuthtypesDeprecatedPostableUserRole:
properties:
id:
type: string
required:
- id
type: object
AuthtypesGettableAuthDomain:
properties:
authNProviderInfo:
@@ -667,20 +660,17 @@ components:
type: string
userRoles:
items:
$ref: '#/components/schemas/AuthtypesDeprecatedPostableUserRole'
$ref: '#/components/schemas/AuthtypesPostableUserRole'
type: array
required:
- email
type: object
AuthtypesPostableUserRole:
properties:
roleId:
type: string
userId:
id:
type: string
required:
- userId
- roleId
- id
type: object
AuthtypesRelation:
enum:
@@ -2927,13 +2917,6 @@ components:
publicDashboard:
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
type: object
DashboardtypesGettablePublicDashboardDataV2:
properties:
dashboard:
$ref: '#/components/schemas/DashboardtypesGettableDashboardV2'
publicDashboard:
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
type: object
DashboardtypesHistogramBuckets:
properties:
bucketCount:
@@ -5241,16 +5224,6 @@ components:
- offset
- limit
type: object
LlmpricingruletypesGettableUnmappedModels:
properties:
items:
items:
$ref: '#/components/schemas/LlmpricingruletypesUnmappedModel'
nullable: true
type: array
required:
- items
type: object
LlmpricingruletypesLLMPricingCacheCosts:
properties:
mode:
@@ -5340,19 +5313,6 @@ components:
type: string
nullable: true
type: array
LlmpricingruletypesUnmappedModel:
properties:
modelName:
type: string
provider:
type: string
spanCount:
minimum: 0
type: integer
required:
- modelName
- spanCount
type: object
LlmpricingruletypesUpdatableLLMPricingRule:
properties:
enabled:
@@ -7440,13 +7400,6 @@ components:
enum:
- basic
type: string
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
properties:
id:
type: string
required:
- id
type: object
ServiceaccounttypesGettableFactorAPIKey:
properties:
createdAt:
@@ -7503,13 +7456,10 @@ components:
type: object
ServiceaccounttypesPostableServiceAccountRole:
properties:
roleId:
type: string
serviceAccountId:
id:
type: string
required:
- serviceAccountId
- roleId
- id
type: object
ServiceaccounttypesServiceAccount:
properties:
@@ -11240,60 +11190,6 @@ paths:
summary: Get a pricing rule
tags:
- llmpricingrules
/api/v1/llm_pricing_rules/unmapped_models:
get:
deprecated: false
description: Returns models seen in the last hour of trace data (gen_ai.request.model)
that no pricing rule pattern matches, so the user can add them to an existing
rule or create a new one.
operationId: ListUnmappedLLMModels
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/LlmpricingruletypesGettableUnmappedModels'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: List unmapped models
tags:
- llmpricingrules
/api/v1/logs/promote_paths:
get:
deprecated: false
@@ -12272,188 +12168,6 @@ paths:
summary: Update route policy
tags:
- routepolicies
/api/v1/service_account_roles:
post:
deprecated: false
description: This endpoint assigns a role to a service account
operationId: CreateServiceAccountRole
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccountRole'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TypesIdentifiable'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- serviceaccount:attach
- role:attach
- tokenizer:
- serviceaccount:attach
- role:attach
summary: Create service account role
tags:
- serviceaccount
/api/v1/service_account_roles/{id}:
delete:
deprecated: false
description: This endpoint revokes a role from a service account
operationId: DeleteServiceAccountRole
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- serviceaccount:detach
- role:detach
- tokenizer:
- serviceaccount:detach
- role:detach
summary: Delete service account role
tags:
- serviceaccount
get:
deprecated: false
description: This endpoint gets an existing service account role
operationId: GetServiceAccountRole
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/ServiceaccounttypesServiceAccountRole'
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:
- serviceaccount:read
- tokenizer:
- serviceaccount:read
summary: Get service account role
tags:
- serviceaccount
/api/v1/service_accounts:
get:
deprecated: false
@@ -13023,9 +12737,9 @@ paths:
tags:
- serviceaccount
post:
deprecated: true
deprecated: false
description: This endpoint assigns a role to a service account
operationId: CreateServiceAccountRoleDeprecated
operationId: CreateServiceAccountRole
parameters:
- in: path
name: id
@@ -13036,7 +12750,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/ServiceaccounttypesDeprecatedPostableServiceAccountRole'
$ref: '#/components/schemas/ServiceaccounttypesPostableServiceAccountRole'
responses:
"201":
content:
@@ -13088,9 +12802,9 @@ paths:
- serviceaccount
/api/v1/service_accounts/{id}/roles/{rid}:
delete:
deprecated: true
deprecated: false
description: This endpoint revokes a role from service account
operationId: DeleteServiceAccountRoleDeprecated
operationId: DeleteServiceAccountRole
parameters:
- in: path
name: id
@@ -17727,138 +17441,6 @@ paths:
summary: Update my organization
tags:
- orgs
/api/v2/public/dashboards/{id}:
get:
deprecated: false
description: This endpoint returns the sanitized v2-shape dashboard data for
public access. Each panel query is reduced to a safe field subset, so filters
and raw query strings are not exposed.
operationId: GetPublicDashboardDataV2
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesGettablePublicDashboardDataV2'
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:
- anonymous:
- public-dashboard:read
summary: Get public dashboard data (v2)
tags:
- dashboard
/api/v2/public/dashboards/{id}/panels/{key}/query_range:
get:
deprecated: false
description: This endpoint returns query range results for a panel of a v2-shape
public dashboard. The panel is addressed by its key in spec.panels.
operationId: GetPublicDashboardPanelQueryRangeV2
parameters:
- in: path
name: id
required: true
schema:
type: string
- in: path
name: key
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeResponse'
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:
- anonymous:
- public-dashboard:read
summary: Get query range result (v2)
tags:
- dashboard
/api/v2/readyz:
get:
operationId: Readyz
@@ -22192,184 +21774,6 @@ paths:
summary: Rotate session
tags:
- sessions
/api/v2/user_roles:
post:
deprecated: false
description: This endpoint assigns a role to a user
operationId: CreateUserRole
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/AuthtypesPostableUserRole'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/TypesIdentifiable'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create user role
tags:
- users
/api/v2/user_roles/{id}:
delete:
deprecated: false
description: This endpoint revokes a role from a user
operationId: DeleteUserRole
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"204":
description: No Content
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Delete user role
tags:
- users
get:
deprecated: false
description: This endpoint gets an existing user role
operationId: GetUserRole
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/AuthtypesUserRole'
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:
- ADMIN
- tokenizer:
- ADMIN
summary: Get user role
tags:
- users
/api/v2/users:
get:
deprecated: false
@@ -22823,7 +22227,7 @@ paths:
tags:
- users
post:
deprecated: true
deprecated: false
description: This endpoint assigns the role to the user roles by user id
operationId: SetRoleByUserID
parameters:
@@ -22874,7 +22278,7 @@ paths:
- users
/api/v2/users/{id}/roles/{roleId}:
delete:
deprecated: true
deprecated: false
description: This endpoint removes a role from the user by user id and role
id
operationId: RemoveUserRoleByUserIDAndRoleID

View File

@@ -519,7 +519,7 @@ func (module *module) getOrCreateAPIKey(ctx context.Context, orgID valuer.UUID,
if err != nil {
return "", err
}
_, err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName)
err = module.serviceAccount.SetRoleByName(ctx, orgID, serviceAccount.ID, authtypes.SigNozViewerRoleName)
if err != nil {
return "", err
}

View File

@@ -29,7 +29,6 @@ type module struct {
settings factory.ScopedProviderSettings
querier querier.Querier
licensing licensing.Licensing
tagModule tag.Module
}
func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, analytics analytics.Analytics, orgGetter organization.Getter, queryParser queryparser.QueryParser, querier querier.Querier, licensing licensing.Licensing, tagModule tag.Module) dashboard.Module {
@@ -42,7 +41,6 @@ func NewModule(store dashboardtypes.Store, settings factory.ProviderSettings, an
settings: scopedProviderSettings,
querier: querier,
licensing: licensing,
tagModule: tagModule,
}
}
@@ -134,55 +132,6 @@ func (module *module) GetPublicWidgetQueryRange(ctx context.Context, id valuer.U
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
}
func (module *module) GetDashboardByPublicIDV2(ctx context.Context, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
if err != nil {
return nil, err
}
tags, err := module.tagModule.ListForResource(ctx, storableDashboard.OrgID, coretypes.KindDashboard, storableDashboard.ID)
if err != nil {
return nil, err
}
return storableDashboard.ToDashboardV2(tags)
}
func (module *module) GetPublicWidgetQueryRangeV2(ctx context.Context, id valuer.UUID, panelKey, startTimeRaw, endTimeRaw string) (*querybuildertypesv5.QueryRangeResponse, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.CodeNamespace: "dashboard",
instrumentationtypes.CodeFunctionName: "GetPublicWidgetQueryRangeV2",
})
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
if err != nil {
return nil, err
}
// tags are not needed for query range.
dashboard, err := storableDashboard.ToDashboardV2(nil)
if err != nil {
return nil, err
}
publicDashboard, err := module.GetPublic(ctx, dashboard.OrgID, dashboard.ID)
if err != nil {
return nil, err
}
startTime, endTime, err := publicDashboard.ResolveTimeRange(startTimeRaw, endTimeRaw)
if err != nil {
return nil, err
}
query, err := dashboard.GetPanelQuery(startTime, endTime, panelKey)
if err != nil {
return nil, err
}
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
}
func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publicDashboard *dashboardtypes.PublicDashboard) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {

View File

@@ -141,7 +141,7 @@ func (ah *APIHandler) getOrCreateCloudIntegrationServiceAccount(ctx context.Cont
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
}
_, err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName)
err = ah.Signoz.Modules.ServiceAccount.SetRoleByName(ctx, orgId, cloudIntegrationServiceAccount.ID, authtypes.SigNozViewerRoleName)
if err != nil {
return nil, basemodel.InternalError(fmt.Errorf("couldn't create cloud integration service account: %w", err))
}

View File

@@ -293,8 +293,6 @@
// Forces subpath imports (@signozhq/ui/<component>) instead of the eagerly-loaded barrel
"signoz/no-css-module-bracket-access": "warn",
// Prevents bracket access on CSS modules (styles['kebab-case']) which fails with camelCaseOnly config
"signoz/no-dashboard-fetch-outside-root": "error",
// Forces useDashboardFetchRequired() outside the root V2 pages (allowlisted in overrides below)
"no-restricted-globals": [
"error",
{
@@ -560,18 +558,6 @@
"rules": {
"signoz/no-zustand-getstate-in-hooks": "off"
}
},
{
// Root V2 pages own the dashboard fetch lifecycle; useDashboardFetchRequired wraps it.
// Everywhere else must use useDashboardFetchRequired().
"files": [
"src/pages/DashboardPageV2/DashboardPageV2.tsx",
"src/pages/DashboardPageV2/PanelEditorPage/PanelEditorPage.tsx",
"src/pages/DashboardPageV2/DashboardContainer/hooks/useDashboardFetchRequired.ts"
],
"rules": {
"signoz/no-dashboard-fetch-outside-root": "off"
}
}
]
}

View File

@@ -25,7 +25,6 @@ You are operating within a constrained context window and strict system prompts.
- Never create barrel files.
- When writing new css, prefer CSS Modules
- Use ./docs/css-modules-guide.md as reference on how to write good CSS Modules.
- When writing code that could need authorization checks, read ./src/lib/authz/README.md
3. FORCED VERIFICATION: Your internal tools mark file writes as successful even if the code does not compile. You are FORBIDDEN from reporting a task as complete until you have:
- Run `pnpm tsgo --noEmit`

View File

@@ -1,48 +0,0 @@
/**
* Rule: no-dashboard-fetch-outside-root
*
* `useDashboardFetch` owns the V2 dashboard fetch and exposes its loading/error/refetch
* lifecycle. That lifecycle is a root-page concern: DashboardPageV2 and PanelEditorPage
* gate their subtree on a resolved dashboard before mounting anything below them.
*
* Every other component therefore renders inside an already-loaded subtree and must use
* `useDashboardFetchRequired`, which reuses the same react-query cache entry but guarantees
* a non-undefined dashboard (throwing otherwise) — so consumers don't re-handle states the
* root page already owns.
*
* This rule flags `useDashboardFetch` imports. It is disabled via an override in
* .oxlintrc.json for the two root pages and for useDashboardFetchRequired.ts (which wraps it).
*/
const FETCH_HOOK = 'useDashboardFetch';
export default {
meta: {
type: 'suggestion',
docs: {
description:
'Disallow useDashboardFetch outside the root dashboard pages; use useDashboardFetchRequired instead',
category: 'Dashboard V2',
},
schema: [],
messages: {
useRequired:
'Use useDashboardFetchRequired() instead of useDashboardFetch — the latter is reserved for the root pages (DashboardPageV2, PanelEditorPage) that own the fetch lifecycle. Components below them render inside a loaded subtree and should assume a guaranteed dashboard.',
},
},
create(context) {
return {
ImportDeclaration(node) {
node.specifiers.forEach((specifier) => {
if (
specifier.type === 'ImportSpecifier' &&
specifier.imported.name === FETCH_HOOK
) {
context.report({ node: specifier, messageId: 'useRequired' });
}
});
},
};
},
};

View File

@@ -12,7 +12,6 @@ import noRawAbsolutePath from './rules/no-raw-absolute-path.mjs';
import noAntdComponents from './rules/no-antd-components.mjs';
import noSignozhqUiBarrel from './rules/no-signozhq-ui-barrel.mjs';
import noCssModuleBracketAccess from './rules/no-css-module-bracket-access.mjs';
import noDashboardFetchOutsideRoot from './rules/no-dashboard-fetch-outside-root.mjs';
export default {
meta: {
@@ -26,6 +25,5 @@ export default {
'no-antd-components': noAntdComponents,
'no-signozhq-ui-barrel': noSignozhqUiBarrel,
'no-css-module-bracket-access': noCssModuleBracketAccess,
'no-dashboard-fetch-outside-root': noDashboardFetchOutsideRoot,
},
};

View File

@@ -38,10 +38,6 @@ import type {
GetPublicDashboard200,
GetPublicDashboardData200,
GetPublicDashboardDataPathParameters,
GetPublicDashboardDataV2200,
GetPublicDashboardDataV2PathParameters,
GetPublicDashboardPanelQueryRangeV2200,
GetPublicDashboardPanelQueryRangeV2PathParameters,
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
GetPublicDashboardWidgetQueryRangePathParameters,
@@ -1803,217 +1799,6 @@ export const useLockDashboardV2 = <
> => {
return useMutation(getLockDashboardV2MutationOptions(options));
};
/**
* This endpoint returns the sanitized v2-shape dashboard data for public access. Each panel query is reduced to a safe field subset, so filters and raw query strings are not exposed.
* @summary Get public dashboard data (v2)
*/
export const getPublicDashboardDataV2 = (
{ id }: GetPublicDashboardDataV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetPublicDashboardDataV2200>({
url: `/api/v2/public/dashboards/${id}`,
method: 'GET',
signal,
});
};
export const getGetPublicDashboardDataV2QueryKey = ({
id,
}: GetPublicDashboardDataV2PathParameters) => {
return [`/api/v2/public/dashboards/${id}`] as const;
};
export const getGetPublicDashboardDataV2QueryOptions = <
TData = Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetPublicDashboardDataV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetPublicDashboardDataV2QueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>
> = ({ signal }) => getPublicDashboardDataV2({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetPublicDashboardDataV2QueryResult = NonNullable<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>
>;
export type GetPublicDashboardDataV2QueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get public dashboard data (v2)
*/
export function useGetPublicDashboardDataV2<
TData = Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetPublicDashboardDataV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardDataV2>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPublicDashboardDataV2QueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get public dashboard data (v2)
*/
export const invalidateGetPublicDashboardDataV2 = async (
queryClient: QueryClient,
{ id }: GetPublicDashboardDataV2PathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetPublicDashboardDataV2QueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint returns query range results for a panel of a v2-shape public dashboard. The panel is addressed by its key in spec.panels.
* @summary Get query range result (v2)
*/
export const getPublicDashboardPanelQueryRangeV2 = (
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetPublicDashboardPanelQueryRangeV2200>({
url: `/api/v2/public/dashboards/${id}/panels/${key}/query_range`,
method: 'GET',
signal,
});
};
export const getGetPublicDashboardPanelQueryRangeV2QueryKey = ({
id,
key,
}: GetPublicDashboardPanelQueryRangeV2PathParameters) => {
return [`/api/v2/public/dashboards/${id}/panels/${key}/query_range`] as const;
};
export const getGetPublicDashboardPanelQueryRangeV2QueryOptions = <
TData = Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetPublicDashboardPanelQueryRangeV2QueryKey({ id, key });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>
> = ({ signal }) => getPublicDashboardPanelQueryRangeV2({ id, key }, signal);
return {
queryKey,
queryFn,
enabled: !!(id && key),
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetPublicDashboardPanelQueryRangeV2QueryResult = NonNullable<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>
>;
export type GetPublicDashboardPanelQueryRangeV2QueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get query range result (v2)
*/
export function useGetPublicDashboardPanelQueryRangeV2<
TData = Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardPanelQueryRangeV2>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPublicDashboardPanelQueryRangeV2QueryOptions(
{ id, key },
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get query range result (v2)
*/
export const invalidateGetPublicDashboardPanelQueryRangeV2 = async (
queryClient: QueryClient,
{ id, key }: GetPublicDashboardPanelQueryRangeV2PathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetPublicDashboardPanelQueryRangeV2QueryKey({ id, key }) },
options,
);
return queryClient;
};
/**
* Same as ListDashboardsV2 but personalized for the calling user: each dashboard carries the caller's `pinned` state, and pinned dashboards float to the top of the requested ordering. Supports the same filter DSL, sort, order, and pagination.
* @summary List dashboards for the current user (v2)

View File

@@ -23,7 +23,6 @@ import type {
GetLLMPricingRulePathParameters,
ListLLMPricingRules200,
ListLLMPricingRulesParams,
ListUnmappedLLMModels200,
LlmpricingruletypesUpdatableLLMPricingRulesDTO,
RenderErrorResponseDTO,
} from '../sigNoz.schemas';
@@ -394,87 +393,3 @@ export const invalidateGetLLMPricingRule = async (
return queryClient;
};
/**
* Returns models seen in the last hour of trace data (gen_ai.request.model) that no pricing rule pattern matches, so the user can add them to an existing rule or create a new one.
* @summary List unmapped models
*/
export const listUnmappedLLMModels = (signal?: AbortSignal) => {
return GeneratedAPIInstance<ListUnmappedLLMModels200>({
url: `/api/v1/llm_pricing_rules/unmapped_models`,
method: 'GET',
signal,
});
};
export const getListUnmappedLLMModelsQueryKey = () => {
return [`/api/v1/llm_pricing_rules/unmapped_models`] as const;
};
export const getListUnmappedLLMModelsQueryOptions = <
TData = Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError,
TData
>;
}) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListUnmappedLLMModelsQueryKey();
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listUnmappedLLMModels>>
> = ({ signal }) => listUnmappedLLMModels(signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListUnmappedLLMModelsQueryResult = NonNullable<
Awaited<ReturnType<typeof listUnmappedLLMModels>>
>;
export type ListUnmappedLLMModelsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary List unmapped models
*/
export function useListUnmappedLLMModels<
TData = Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listUnmappedLLMModels>>,
TError,
TData
>;
}): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListUnmappedLLMModelsQueryOptions(options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary List unmapped models
*/
export const invalidateListUnmappedLLMModels = async (
queryClient: QueryClient,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListUnmappedLLMModelsQueryKey() },
options,
);
return queryClient;
};

View File

@@ -22,16 +22,12 @@ import type {
CreateServiceAccountKey201,
CreateServiceAccountKeyPathParameters,
CreateServiceAccountRole201,
CreateServiceAccountRoleDeprecated201,
CreateServiceAccountRoleDeprecatedPathParameters,
CreateServiceAccountRolePathParameters,
DeleteServiceAccountPathParameters,
DeleteServiceAccountRoleDeprecatedPathParameters,
DeleteServiceAccountRolePathParameters,
GetMyServiceAccount200,
GetServiceAccount200,
GetServiceAccountPathParameters,
GetServiceAccountRole200,
GetServiceAccountRolePathParameters,
GetServiceAccountRoles200,
GetServiceAccountRolesPathParameters,
ListServiceAccountKeys200,
@@ -39,7 +35,6 @@ import type {
ListServiceAccounts200,
RenderErrorResponseDTO,
RevokeServiceAccountKeyPathParameters,
ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO,
ServiceaccounttypesPostableFactorAPIKeyDTO,
ServiceaccounttypesPostableServiceAccountDTO,
ServiceaccounttypesPostableServiceAccountRoleDTO,
@@ -51,272 +46,6 @@ import type {
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
/**
* This endpoint assigns a role to a service account
* @summary Create service account role
*/
export const createServiceAccountRole = (
serviceaccounttypesPostableServiceAccountRoleDTO?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateServiceAccountRole201>({
url: `/api/v1/service_account_roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesPostableServiceAccountRoleDTO,
signal,
});
};
export const getCreateServiceAccountRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
TContext
> => {
const mutationKey = ['createServiceAccountRole'];
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 createServiceAccountRole>>,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> }
> = (props) => {
const { data } = props ?? {};
return createServiceAccountRole(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createServiceAccountRole>>
>;
export type CreateServiceAccountRoleMutationBody =
| BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>
| undefined;
export type CreateServiceAccountRoleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create service account role
*/
export const useCreateServiceAccountRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{ data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO> },
TContext
> => {
return useMutation(getCreateServiceAccountRoleMutationOptions(options));
};
/**
* This endpoint revokes a role from a service account
* @summary Delete service account role
*/
export const deleteServiceAccountRole = (
{ id }: DeleteServiceAccountRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v1/service_account_roles/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteServiceAccountRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
> => {
const mutationKey = ['deleteServiceAccountRole'];
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 deleteServiceAccountRole>>,
{ pathParams: DeleteServiceAccountRolePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteServiceAccountRole(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteServiceAccountRole>>
>;
export type DeleteServiceAccountRoleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete service account role
*/
export const useDeleteServiceAccountRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
> => {
return useMutation(getDeleteServiceAccountRoleMutationOptions(options));
};
/**
* This endpoint gets an existing service account role
* @summary Get service account role
*/
export const getServiceAccountRole = (
{ id }: GetServiceAccountRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetServiceAccountRole200>({
url: `/api/v1/service_account_roles/${id}`,
method: 'GET',
signal,
});
};
export const getGetServiceAccountRoleQueryKey = ({
id,
}: GetServiceAccountRolePathParameters) => {
return [`/api/v1/service_account_roles/${id}`] as const;
};
export const getGetServiceAccountRoleQueryOptions = <
TData = Awaited<ReturnType<typeof getServiceAccountRole>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetServiceAccountRolePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRole>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetServiceAccountRoleQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getServiceAccountRole>>
> = ({ signal }) => getServiceAccountRole({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRole>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetServiceAccountRoleQueryResult = NonNullable<
Awaited<ReturnType<typeof getServiceAccountRole>>
>;
export type GetServiceAccountRoleQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get service account role
*/
export function useGetServiceAccountRole<
TData = Awaited<ReturnType<typeof getServiceAccountRole>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetServiceAccountRolePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getServiceAccountRole>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetServiceAccountRoleQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get service account role
*/
export const invalidateGetServiceAccountRole = async (
queryClient: QueryClient,
{ id }: GetServiceAccountRolePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetServiceAccountRoleQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint lists the service accounts for an organisation
* @summary List service accounts
@@ -1255,46 +984,45 @@ export const invalidateGetServiceAccountRoles = async (
/**
* This endpoint assigns a role to a service account
* @deprecated
* @summary Create service account role
*/
export const createServiceAccountRoleDeprecated = (
{ id }: CreateServiceAccountRoleDeprecatedPathParameters,
serviceaccounttypesDeprecatedPostableServiceAccountRoleDTO?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>,
export const createServiceAccountRole = (
{ id }: CreateServiceAccountRolePathParameters,
serviceaccounttypesPostableServiceAccountRoleDTO?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateServiceAccountRoleDeprecated201>({
return GeneratedAPIInstance<CreateServiceAccountRole201>({
url: `/api/v1/service_accounts/${id}/roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: serviceaccounttypesDeprecatedPostableServiceAccountRoleDTO,
data: serviceaccounttypesPostableServiceAccountRoleDTO,
signal,
});
};
export const getCreateServiceAccountRoleDeprecatedMutationOptions = <
export const getCreateServiceAccountRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
},
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
},
TContext
> => {
const mutationKey = ['createServiceAccountRoleDeprecated'];
const mutationKey = ['createServiceAccountRole'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
@@ -1304,66 +1032,62 @@ export const getCreateServiceAccountRoleDeprecatedMutationOptions = <
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
{
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
}
> = (props) => {
const { pathParams, data } = props ?? {};
return createServiceAccountRoleDeprecated(pathParams, data);
return createServiceAccountRole(pathParams, data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateServiceAccountRoleDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>
export type CreateServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createServiceAccountRole>>
>;
export type CreateServiceAccountRoleDeprecatedMutationBody =
| BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>
export type CreateServiceAccountRoleMutationBody =
| BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>
| undefined;
export type CreateServiceAccountRoleDeprecatedMutationError =
export type CreateServiceAccountRoleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Create service account role
*/
export const useCreateServiceAccountRoleDeprecated = <
export const useCreateServiceAccountRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
},
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof createServiceAccountRole>>,
TError,
{
pathParams: CreateServiceAccountRoleDeprecatedPathParameters;
data?: BodyType<ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO>;
pathParams: CreateServiceAccountRolePathParameters;
data?: BodyType<ServiceaccounttypesPostableServiceAccountRoleDTO>;
},
TContext
> => {
return useMutation(
getCreateServiceAccountRoleDeprecatedMutationOptions(options),
);
return useMutation(getCreateServiceAccountRoleMutationOptions(options));
};
/**
* This endpoint revokes a role from service account
* @deprecated
* @summary Delete service account role
*/
export const deleteServiceAccountRoleDeprecated = (
{ id, rid }: DeleteServiceAccountRoleDeprecatedPathParameters,
export const deleteServiceAccountRole = (
{ id, rid }: DeleteServiceAccountRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
@@ -1373,23 +1097,23 @@ export const deleteServiceAccountRoleDeprecated = (
});
};
export const getDeleteServiceAccountRoleDeprecatedMutationOptions = <
export const getDeleteServiceAccountRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
> => {
const mutationKey = ['deleteServiceAccountRoleDeprecated'];
const mutationKey = ['deleteServiceAccountRole'];
const { mutation: mutationOptions } = options
? options.mutation &&
'mutationKey' in options.mutation &&
@@ -1399,47 +1123,44 @@ export const getDeleteServiceAccountRoleDeprecatedMutationOptions = <
: { mutation: { mutationKey } };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters }
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
{ pathParams: DeleteServiceAccountRolePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteServiceAccountRoleDeprecated(pathParams);
return deleteServiceAccountRole(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteServiceAccountRoleDeprecatedMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>
export type DeleteServiceAccountRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteServiceAccountRole>>
>;
export type DeleteServiceAccountRoleDeprecatedMutationError =
export type DeleteServiceAccountRoleMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Delete service account role
*/
export const useDeleteServiceAccountRoleDeprecated = <
export const useDeleteServiceAccountRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteServiceAccountRoleDeprecated>>,
Awaited<ReturnType<typeof deleteServiceAccountRole>>,
TError,
{ pathParams: DeleteServiceAccountRoleDeprecatedPathParameters },
{ pathParams: DeleteServiceAccountRolePathParameters },
TContext
> => {
return useMutation(
getDeleteServiceAccountRoleDeprecatedMutationOptions(options),
);
return useMutation(getDeleteServiceAccountRoleMutationOptions(options));
};
/**
* This endpoint gets my service account

View File

@@ -2048,13 +2048,6 @@ export interface AuthtypesAuthNSupportDTO {
password?: AuthtypesPasswordAuthNSupportDTO[] | null;
}
export interface AuthtypesDeprecatedPostableUserRoleDTO {
/**
* @type string
*/
id: string;
}
export interface AuthtypesGettableAuthDomainDTO {
authNProviderInfo?: AuthtypesAuthNProviderInfoDTO;
config?: AuthtypesAuthDomainConfigDTO;
@@ -2294,6 +2287,13 @@ export interface AuthtypesPostableRotateTokenDTO {
refreshToken?: string;
}
export interface AuthtypesPostableUserRoleDTO {
/**
* @type string
*/
id: string;
}
export interface AuthtypesPostableUserDTO {
/**
* @type string
@@ -2310,18 +2310,7 @@ export interface AuthtypesPostableUserDTO {
/**
* @type array
*/
userRoles?: AuthtypesDeprecatedPostableUserRoleDTO[];
}
export interface AuthtypesPostableUserRoleDTO {
/**
* @type string
*/
roleId: string;
/**
* @type string
*/
userId: string;
userRoles?: AuthtypesPostableUserRoleDTO[];
}
export interface AuthtypesRoleDTO {
@@ -4952,11 +4941,6 @@ export interface DashboardtypesGettablePublicDashboardDataDTO {
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
}
export interface DashboardtypesGettablePublicDashboardDataV2DTO {
dashboard?: DashboardtypesGettableDashboardV2DTO;
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
}
export enum DashboardtypesPatchOpDTO {
add = 'add',
remove = 'remove',
@@ -6834,29 +6818,6 @@ export interface LlmpricingruletypesGettablePricingRulesDTO {
total: number;
}
export interface LlmpricingruletypesUnmappedModelDTO {
/**
* @type string
*/
modelName: string;
/**
* @type string
*/
provider?: string;
/**
* @type integer
* @minimum 0
*/
spanCount: number;
}
export interface LlmpricingruletypesGettableUnmappedModelsDTO {
/**
* @type array,null
*/
items: LlmpricingruletypesUnmappedModelDTO[] | null;
}
export interface LlmpricingruletypesUpdatableLLMPricingRuleDTO {
/**
* @type boolean
@@ -8519,13 +8480,6 @@ export interface RuletypesRuleDTO {
export enum RuletypesThresholdKindDTO {
basic = 'basic',
}
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {
/**
* @type string
*/
id: string;
}
export interface ServiceaccounttypesGettableFactorAPIKeyDTO {
/**
* @type string
@@ -8595,11 +8549,7 @@ export interface ServiceaccounttypesPostableServiceAccountRoleDTO {
/**
* @type string
*/
roleId: string;
/**
* @type string
*/
serviceAccountId: string;
id: string;
}
export interface ServiceaccounttypesServiceAccountDTO {
@@ -10218,14 +10168,6 @@ export type GetLLMPricingRule200 = {
status: string;
};
export type ListUnmappedLLMModels200 = {
data: LlmpricingruletypesGettableUnmappedModelsDTO;
/**
* @type string
*/
status: string;
};
export type ListPromotedAndIndexedPaths200 = {
/**
* @type array,null
@@ -10365,28 +10307,6 @@ export type UpdateRoutePolicy200 = {
status: string;
};
export type CreateServiceAccountRole201 = {
data: TypesIdentifiableDTO;
/**
* @type string
*/
status: string;
};
export type DeleteServiceAccountRolePathParameters = {
id: string;
};
export type GetServiceAccountRolePathParameters = {
id: string;
};
export type GetServiceAccountRole200 = {
data: ServiceaccounttypesServiceAccountRoleDTO;
/**
* @type string
*/
status: string;
};
export type ListServiceAccounts200 = {
/**
* @type array
@@ -10470,10 +10390,10 @@ export type GetServiceAccountRoles200 = {
status: string;
};
export type CreateServiceAccountRoleDeprecatedPathParameters = {
export type CreateServiceAccountRolePathParameters = {
id: string;
};
export type CreateServiceAccountRoleDeprecated201 = {
export type CreateServiceAccountRole201 = {
data: TypesIdentifiableDTO;
/**
* @type string
@@ -10481,7 +10401,7 @@ export type CreateServiceAccountRoleDeprecated201 = {
status: string;
};
export type DeleteServiceAccountRoleDeprecatedPathParameters = {
export type DeleteServiceAccountRolePathParameters = {
id: string;
rid: string;
};
@@ -11241,29 +11161,6 @@ export type GetMyOrganization200 = {
status: string;
};
export type GetPublicDashboardDataV2PathParameters = {
id: string;
};
export type GetPublicDashboardDataV2200 = {
data: DashboardtypesGettablePublicDashboardDataV2DTO;
/**
* @type string
*/
status: string;
};
export type GetPublicDashboardPanelQueryRangeV2PathParameters = {
id: string;
key: string;
};
export type GetPublicDashboardPanelQueryRangeV2200 = {
data: Querybuildertypesv5QueryRangeResponseDTO;
/**
* @type string
*/
status: string;
};
export type Readyz200 = {
data: FactoryResponseDTO;
/**
@@ -11641,28 +11538,6 @@ export type RotateSession200 = {
status: string;
};
export type CreateUserRole201 = {
data: TypesIdentifiableDTO;
/**
* @type string
*/
status: string;
};
export type DeleteUserRolePathParameters = {
id: string;
};
export type GetUserRolePathParameters = {
id: string;
};
export type GetUserRole200 = {
data: AuthtypesUserRoleDTO;
/**
* @type string
*/
status: string;
};
export type ListUsers200 = {
/**
* @type array

View File

@@ -19,15 +19,12 @@ import type {
import type {
AuthtypesPostableUserDTO,
AuthtypesPostableUserRoleDTO,
CreateInvite201,
CreateResetPasswordToken201,
CreateResetPasswordTokenPathParameters,
CreateUser201,
CreateUserRole201,
DeleteUserDeprecatedPathParameters,
DeleteUserPathParameters,
DeleteUserRolePathParameters,
GetMyUser200,
GetMyUserDeprecated200,
GetResetPasswordToken200,
@@ -40,8 +37,6 @@ import type {
GetUserDeprecated200,
GetUserDeprecatedPathParameters,
GetUserPathParameters,
GetUserRole200,
GetUserRolePathParameters,
GetUsersByRoleID200,
GetUsersByRoleIDPathParameters,
ListUsers200,
@@ -1159,267 +1154,6 @@ export const invalidateGetUsersByRoleID = async (
return queryClient;
};
/**
* This endpoint assigns a role to a user
* @summary Create user role
*/
export const createUserRole = (
authtypesPostableUserRoleDTO?: BodyType<AuthtypesPostableUserRoleDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateUserRole201>({
url: `/api/v2/user_roles`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: authtypesPostableUserRoleDTO,
signal,
});
};
export const getCreateUserRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createUserRole>>,
TError,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createUserRole>>,
TError,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
TContext
> => {
const mutationKey = ['createUserRole'];
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 createUserRole>>,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> }
> = (props) => {
const { data } = props ?? {};
return createUserRole(data);
};
return { mutationFn, ...mutationOptions };
};
export type CreateUserRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof createUserRole>>
>;
export type CreateUserRoleMutationBody =
| BodyType<AuthtypesPostableUserRoleDTO>
| undefined;
export type CreateUserRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Create user role
*/
export const useCreateUserRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createUserRole>>,
TError,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof createUserRole>>,
TError,
{ data?: BodyType<AuthtypesPostableUserRoleDTO> },
TContext
> => {
return useMutation(getCreateUserRoleMutationOptions(options));
};
/**
* This endpoint revokes a role from a user
* @summary Delete user role
*/
export const deleteUserRole = (
{ id }: DeleteUserRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<void>({
url: `/api/v2/user_roles/${id}`,
method: 'DELETE',
signal,
});
};
export const getDeleteUserRoleMutationOptions = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUserRole>>,
TError,
{ pathParams: DeleteUserRolePathParameters },
TContext
>;
}): UseMutationOptions<
Awaited<ReturnType<typeof deleteUserRole>>,
TError,
{ pathParams: DeleteUserRolePathParameters },
TContext
> => {
const mutationKey = ['deleteUserRole'];
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 deleteUserRole>>,
{ pathParams: DeleteUserRolePathParameters }
> = (props) => {
const { pathParams } = props ?? {};
return deleteUserRole(pathParams);
};
return { mutationFn, ...mutationOptions };
};
export type DeleteUserRoleMutationResult = NonNullable<
Awaited<ReturnType<typeof deleteUserRole>>
>;
export type DeleteUserRoleMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Delete user role
*/
export const useDeleteUserRole = <
TError = ErrorType<RenderErrorResponseDTO>,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof deleteUserRole>>,
TError,
{ pathParams: DeleteUserRolePathParameters },
TContext
>;
}): UseMutationResult<
Awaited<ReturnType<typeof deleteUserRole>>,
TError,
{ pathParams: DeleteUserRolePathParameters },
TContext
> => {
return useMutation(getDeleteUserRoleMutationOptions(options));
};
/**
* This endpoint gets an existing user role
* @summary Get user role
*/
export const getUserRole = (
{ id }: GetUserRolePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetUserRole200>({
url: `/api/v2/user_roles/${id}`,
method: 'GET',
signal,
});
};
export const getGetUserRoleQueryKey = ({ id }: GetUserRolePathParameters) => {
return [`/api/v2/user_roles/${id}`] as const;
};
export const getGetUserRoleQueryOptions = <
TData = Awaited<ReturnType<typeof getUserRole>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetUserRolePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUserRole>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getGetUserRoleQueryKey({ id });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getUserRole>>> = ({
signal,
}) => getUserRole({ id }, signal);
return {
queryKey,
queryFn,
enabled: !!id,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getUserRole>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetUserRoleQueryResult = NonNullable<
Awaited<ReturnType<typeof getUserRole>>
>;
export type GetUserRoleQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get user role
*/
export function useGetUserRole<
TData = Awaited<ReturnType<typeof getUserRole>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetUserRolePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getUserRole>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetUserRoleQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Get user role
*/
export const invalidateGetUserRole = async (
queryClient: QueryClient,
{ id }: GetUserRolePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetUserRoleQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint lists all users for the organization
* @summary List users v2
@@ -2131,7 +1865,6 @@ export const invalidateGetRolesByUserID = async (
/**
* This endpoint assigns the role to the user roles by user id
* @deprecated
* @summary Set user roles
*/
export const setRoleByUserID = (
@@ -2203,7 +1936,6 @@ export type SetRoleByUserIDMutationBody =
export type SetRoleByUserIDMutationError = ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Set user roles
*/
export const useSetRoleByUserID = <
@@ -2232,7 +1964,6 @@ export const useSetRoleByUserID = <
};
/**
* This endpoint removes a role from the user by user id and role id
* @deprecated
* @summary Remove a role from user
*/
export const removeUserRoleByUserIDAndRoleID = (
@@ -2291,7 +2022,6 @@ export type RemoveUserRoleByUserIDAndRoleIDMutationError =
ErrorType<RenderErrorResponseDTO>;
/**
* @deprecated
* @summary Remove a role from user
*/
export const useRemoveUserRoleByUserIDAndRoleID = <

View File

@@ -1,7 +1,7 @@
import { QueryParams } from 'constants/query';
export const ExploreHeaderToolTip = {
url: 'https://signoz.io/docs/querying/overview/?utm_source=product&utm_medium=new-query-builder',
url: 'https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=new-query-builder',
text: 'More details on how to use query builder',
};

View File

@@ -20,9 +20,9 @@
padding: 0px 8px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
color: var(--input-with-label-color, var(--l2-foreground));
border: 1px solid var(--l2-border);
background: var(--l2-background);
color: var(--l2-foreground);
display: flex;
justify-content: flex-start;
@@ -35,54 +35,21 @@
min-width: 150px;
font-family: 'Space Mono', monospace !important;
--input-border-radius: 0px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
color: var(--input-with-label-color, var(--l2-foreground));
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
border-right: none;
border-left: none;
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
font-size: 12px !important;
line-height: 25px;
position: relative;
&:hover,
&:focus {
z-index: 1;
}
.ant-select-selector {
position: relative;
border-radius: inherit;
}
.ant-select:hover .ant-select-selector,
.ant-select-focused .ant-select-selector {
z-index: 1;
}
&.input__has-label-after {
margin-left: -1px;
.ant-select-selector {
margin-left: -1px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
&.input__has-close-button {
.ant-select-selector {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
~ .close-btn {
margin-left: -1px;
}
}
line-height: 27px;
&::placeholder {
color: var(--input-with-label-color, var(--l3-foreground)) !important;
color: var(--l2-foreground) !important;
font-size: 12px !important;
}
&[type='number']::-webkit-inner-spin-button,
@@ -96,35 +63,25 @@
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
height: 100%;
border: 1px solid var(--l2-border);
background: var(--l2-background);
height: 38px;
width: 38px;
position: relative;
&:hover,
&:focus {
z-index: 2;
}
}
&.labelAfter {
.input {
border-radius: 2px;
border: 1px solid var(--input-with-label-border-color, var(--l2-border));
background: var(--input-with-label-background-color, var(--l2-background));
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--l1-border);
background: var(--l2-background);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
.ant-select-selector {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
.label {
border-left: none;
border-radius: 0px 2px 2px 0px;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
}
}

View File

@@ -45,10 +45,7 @@ function InputWithLabel({
>
{!labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
<Input
className={cx('input', {
'input__has-label-after': !labelAfter,
'input__has-close-button': !!onClose,
})}
className="input"
placeholder={placeholder}
type={type}
value={inputValue}

View File

@@ -80,8 +80,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -101,8 +101,7 @@
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted
var(--query-builder-v2-border-color, var(--l2-border));
border-left: 6px dotted var(--l1-border);
}
/* Horizontal line pointing from vertical to the item */
@@ -115,8 +114,8 @@
height: 1px;
background: repeating-linear-gradient(
to right,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -242,8 +241,7 @@
top: 12px;
width: 6px;
height: 6px;
border-left: 6px dotted
var(--query-builder-v2-border-color, var(--l2-border));
border-left: 6px dotted var(--l1-border);
}
/* Horizontal line pointing from vertical to the item */
@@ -256,8 +254,8 @@
height: 1px;
background: repeating-linear-gradient(
to right,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -275,16 +273,6 @@
line-height: 16px; /* 128.571% */
resize: none;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
&:placeholder {
color: var(--query-builder-v2-placeholder-color, var(--l3-foreground));
}
}
.formula-legend {
@@ -294,42 +282,15 @@
.ant-input-group-addon {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
background: var(
--query-builder-v2-background-color,
var(--l2-background)
);
color: var(--query-builder-v2-color, var(--l2-foreground));
background: var(--l2-background);
color: var(--l2-foreground);
font-size: 12px;
font-weight: 300;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
position: relative;
}
.ant-input {
border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important;
height: 36px;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
position: relative;
margin-left: -1px;
&:hover,
&:focus {
z-index: 1;
border-color: var(--internal-ant-border-color-hover);
}
&:placeholder {
color: var(--query-builder-v2-placeholder-color, var(--l3-foreground));
}
}
}
}
@@ -362,8 +323,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -434,8 +395,8 @@
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--query-builder-v2-border-color, var(--l2-border)),
var(--query-builder-v2-border-color, var(--l2-border)) 4px,
var(--l1-border),
var(--l1-border) 4px,
transparent 4px,
transparent 8px
);
@@ -451,7 +412,7 @@
min-width: 120px;
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
background: var(--l1-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
@@ -496,16 +457,13 @@
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
height: 36px !important;
border: 1px solid var(--l1-border) !important;
background: var(--l1-background) !important;
height: 34px !important;
box-sizing: border-box !important;
.ant-select-selection-item {
color: var(--query-builder-v2-color, var(--l1-foreground));
color: var(--l1-foreground);
}
}

View File

@@ -92,11 +92,6 @@
.ant-select {
width: 100%;
.ant-select-selector {
min-height: 36px;
}
.ant-select-selection-search-input {
min-width: max-content !important;
max-width: 100% !important;
@@ -105,12 +100,9 @@
.ant-select-selector {
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground));
border: 1.005px solid var(--l1-border);
background: var(--l1-background);
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
@@ -131,7 +123,6 @@
.input {
flex: initial;
width: 100px !important;
min-height: 36px;
}
}
}

View File

@@ -131,7 +131,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#temporal-aggregation-within-each-time-series"
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}
@@ -254,7 +254,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#temporal-aggregation-within-each-time-series"
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}

View File

@@ -8,9 +8,9 @@
.ant-select-selection-search-input {
font-size: 12px !important;
line-height: 25px;
line-height: 27px;
&::placeholder {
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
color: var(--l2-foreground) !important;
font-size: 12px !important;
}
}
@@ -22,12 +22,9 @@
.ant-select-selector {
width: 100%;
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border)) !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground));
border: 1px solid var(--l1-border) !important;
background: var(--l1-background);
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
@@ -36,49 +33,36 @@
min-height: 36px;
.ant-select-selection-placeholder {
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
color: var(--l2-foreground) !important;
font-size: 12px !important;
}
}
}
.qb-select-popover.ant-select-dropdown {
border-radius: 4px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.ant-select-dropdown {
border-radius: 4px;
border: 1px solid var(--l1-border);
background: var(--l1-background);
box-shadow: 4px 10px 16px 2px rgba(0, 0, 0, 0.2);
backdrop-filter: blur(20px);
.ant-select-item {
color: var(--query-builder-v2-color, var(--l2-foreground));
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
.ant-select-item {
color: var(--l1-foreground);
font-family: 'Geist Mono';
font-size: 13px;
font-style: normal;
font-weight: 400;
line-height: 20px; /* 142.857% */
&:not(:last-of-type) {
margin-bottom: 4px;
}
&:hover,
&.ant-select-item-option-active {
background: var(--l3-background) !important;
}
&:hover,
&.ant-select-item-option-active {
background: var(
--query-builder-v2-selected-background-color,
var(--l3-background)
) !important;
}
&.ant-select-item-option-selected {
background: var(
--query-builder-v2-selected-background-color,
var(--l3-background)
) !important;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
font-weight: 600;
&.ant-select-item-option-selected {
background: var(--l3-background) !important;
border: 1px solid var(--l1-border);
font-weight: 600;
}
}
}
}

View File

@@ -142,7 +142,6 @@ export const MetricsSelect = memo(function MetricsSelect({
{signalSourceChangeEnabled && (
<Select
className="source-selector"
popupClassName="qb-select-popover"
placeholder="Source"
options={SOURCE_OPTIONS}
value={source}

View File

@@ -1,23 +1,8 @@
// TODO: Improve the styling of the query aggregation container and its components. - @YounixM , @H4ad
.query-add-ons {
width: 100%;
--toggle-group-secondary-bg: var(
--query-builder-v2-toggle-group-background-color,
var(--l1-background-hover)
);
--toggle-group-secondary-border: var(
--query-builder-v2-toggle-group-border-color,
var(--l2-border)
);
--toggle-group-secondary-active-bg: var(
--query-builder-v2-toggle-group-active-background-color,
var(--l1-background)
);
--toggle-group-secondary-bg-hover: var(
--query-builder-v2-toggle-group-background-color-hover,
var(--l2-background)
);
.add-on-tab-title {
display: flex;
align-items: center;
@@ -44,33 +29,32 @@
font-style: normal;
font-weight: var(--font-weight-normal);
color: var(--query-builder-v2-color, var(--l2-foreground));
color: var(--l2-foreground);
}
> button {
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
border-left: none;
min-width: 120px;
height: 36px;
line-height: 36px;
&:first-child {
border-left: 1px solid
var(--query-builder-v2-border-color, var(--l2-border));
border-left: 1px solid var(--l1-border);
}
&::before {
background: var(--query-builder-v2-border-color, var(--l2-border));
background: var(--l1-border);
}
&[data-state='on'] {
color: var(--text-robin-500);
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
display: none;
&::before {
background: var(--query-builder-v2-border-color, var(--l2-border));
background: var(--l1-border);
}
}
}
@@ -81,7 +65,7 @@
height: 30px;
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
background: var(--l3-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}
@@ -94,13 +78,10 @@
align-items: center;
.having-filter-select-container {
position: relative;
width: 100%;
display: flex;
flex-direction: row;
align-items: flex-start;
background: var(--query-builder-v2-background-color, var(--l2-background));
padding-right: 38px;
align-items: center;
.having-filter-select-editor {
border-radius: 2px;
@@ -125,17 +106,15 @@
}
.cm-content {
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-left-width: 0px;
border-right-width: 0px;
border-radius: 2px;
border: 1px solid var(--l1-border);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
padding: 0px !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
background-color: var(--l2-background) !important;
&:focus-within {
border-color: var(--query-builder-v2-border-color, var(--l2-border));
border-color: var(--l1-border);
}
}
@@ -239,32 +218,17 @@
}
.cm-line {
min-height: 34px;
line-height: 32px !important;
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
background-color: var(--l2-background) !important;
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
::selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
@@ -273,11 +237,8 @@
}
.chip-decorator {
background: var(
--query-builder-v2-chip-decorator-background-color,
var(--l3-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
background: var(--l3-background) !important;
color: var(--l1-foreground) !important;
border-radius: 4px;
padding: 2px 4px;
margin-right: 4px;
@@ -285,38 +246,34 @@
}
.cm-selectionBackground {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
.cm-activeLine > span {
font-size: 12px !important;
}
.cm-placeholder {
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
}
}
}
.close-btn {
position: absolute;
top: 0;
right: 0;
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
height: 100%;
border: 1px solid var(--l2-border);
background: var(--l2-background);
height: 38px;
width: 38px;
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
&:focus:not(:focus-visible),
&.ant-btn:focus:not(:focus-visible) {
border-color: var(--l2-border);
border-left-color: transparent;
outline: none;
box-shadow: none;
}
}
}
}
@@ -343,8 +300,20 @@
font-size: 12px !important;
}
input {
min-height: 36px;
$add-on-row-height: 38px;
.periscope-input-with-label {
.input {
.ant-select {
height: $add-on-row-height;
}
}
}
.input-with-label {
.input {
height: $add-on-row-height;
}
}
}
}

View File

@@ -52,7 +52,7 @@ const ADD_ONS = [
key: ADD_ONS_KEYS.GROUP_BY,
description:
'Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments.',
docLink: 'https://signoz.io/docs/querying/aggregation-grouping/#grouping',
docLink: 'https://signoz.io/docs/userguide/query-builder-v5/#grouping',
},
{
icon: <ScrollText size={14} />,
@@ -61,7 +61,7 @@ const ADD_ONS = [
description:
'Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500',
docLink:
'https://signoz.io/docs/querying/result-manipulation/#conditional-filtering-with-having',
'https://signoz.io/docs/userguide/query-builder-v5/#conditional-filtering-with-having',
},
{
icon: <ScrollText size={14} />,
@@ -70,7 +70,7 @@ const ADD_ONS = [
description:
'Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers.',
docLink:
'https://signoz.io/docs/querying/result-manipulation/#sorting--limiting',
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
},
{
icon: <ScrollText size={14} />,
@@ -79,7 +79,7 @@ const ADD_ONS = [
description:
'Show only the top/bottom N results. Perfect for focusing on outliers, reducing noise, and improving dashboard performance.',
docLink:
'https://signoz.io/docs/querying/result-manipulation/#how-limit-works-for-time-series',
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
},
{
icon: <ScrollText size={14} />,
@@ -88,7 +88,7 @@ const ADD_ONS = [
description:
'Customize series labels using variables like {{service.name}}-{{endpoint}}. Makes charts readable at a glance during incident investigation.',
docLink:
'https://signoz.io/docs/querying/aggregation-grouping/#legend-formatting',
'https://signoz.io/docs/userguide/query-builder-v5/#legend-formatting',
},
];
@@ -99,7 +99,7 @@ const REDUCE_TO = {
description:
'Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value.',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#result-manipulation',
'https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations',
};
const hasValue = (value: unknown): boolean =>
@@ -349,7 +349,7 @@ function QueryAddOns({
<TooltipContent
label="Group By"
description="Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments."
docLink="https://signoz.io/docs/querying/aggregation-grouping/#grouping"
docLink="https://signoz.io/docs/userguide/query-builder-v5/#grouping"
/>
}
placement="top"
@@ -385,7 +385,7 @@ function QueryAddOns({
<TooltipContent
label="Having"
description="Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500"
docLink="https://signoz.io/docs/querying/result-manipulation/#conditional-filtering-with-having"
docLink="https://signoz.io/docs/userguide/query-builder-v5/#conditional-filtering-with-having"
/>
}
placement="top"
@@ -434,7 +434,7 @@ function QueryAddOns({
<TooltipContent
label="Order By"
description="Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers."
docLink="https://signoz.io/docs/querying/result-manipulation/#sorting--limiting"
docLink="https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting"
/>
}
placement="top"
@@ -473,7 +473,7 @@ function QueryAddOns({
<TooltipContent
label="Reduce to"
description="Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value."
docLink="https://signoz.io/docs/userguide/query-builder-v5/#result-manipulation"
docLink="https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations"
/>
}
placement="top"

View File

@@ -23,7 +23,7 @@
flex: 1;
min-width: 0;
font-size: 12px;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
color: var(--l2-foreground) !important;
&.error {
.cm-editor {
@@ -51,15 +51,14 @@
.cm-content {
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
padding: 0px !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
background-color: var(--l1-background) !important;
&:focus-within {
border-color: var(--query-builder-v2-border-color, var(--l2-border));
border-color: var(--l1-border);
}
}
@@ -75,7 +74,7 @@
right: 0px !important;
border-radius: 4px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
background: linear-gradient(
139deg,
color-mix(in srgb, var(--card) 80%, transparent) 0%,
@@ -119,7 +118,7 @@
box-sizing: border-box;
overflow: hidden;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
color: var(--l2-foreground) !important;
font-family: 'Space Mono', monospace !important;
.cm-completionIcon {
@@ -128,10 +127,7 @@
&:hover,
&[aria-selected='true'] {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
color: var(--l1-foreground) !important;
font-weight: 600 !important;
}
@@ -146,24 +142,15 @@
.cm-line {
line-height: 36px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
background-color: var(--l2-background) !important;
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
::selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
@@ -172,11 +159,8 @@
}
.chip-decorator {
background: var(
--query-builder-v2-chip-decorator-background-color,
var(--l3-background)
) !important;
color: var(--query-builder-v2-color, var(--l1-foreground)) !important;
background: var(--l3-background) !important;
color: var(--l1-foreground) !important;
border-radius: 4px;
padding: 2px 4px;
margin-right: 4px;
@@ -184,10 +168,7 @@
}
.cm-selectionBackground {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
}
@@ -220,11 +201,12 @@
.close-btn {
border-radius: 0px 2px 2px 0px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
height: 100%;
border: 1px solid var(--l1-border);
background: var(--l1-background);
height: 38px;
width: 38px;
border-left: transparent;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
}
@@ -235,13 +217,13 @@
height: 36px;
line-height: 36px;
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
font-family: 'Space Mono', monospace !important;
&::placeholder {
color: var(--query-builder-v2-color, var(--l2-foreground));
color: var(--l1-foreground);
opacity: 0.5;
}
}
@@ -256,10 +238,9 @@
.query-aggregation-interval-input-container {
.query-aggregation-interval-input {
input {
min-height: 36px;
max-width: 120px;
&::placeholder {
color: var(--query-builder-v2-color, var(--l2-foreground));
color: var(--l2-foreground);
}
}
}
@@ -270,8 +251,8 @@
.query-aggregation-error-popover {
.ant-popover-inner {
background-color: var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background-color: var(--l1-border);
border: 1px solid var(--l1-border);
border-radius: 4px;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
}

View File

@@ -65,7 +65,7 @@ function QueryAggregationOptions({
Set the time interval for aggregation
<br />
<a
href="https://signoz.io/docs/userguide/query-builder-v5/#temporal-aggregation-within-each-time-series"
href="https://signoz.io/docs/userguide/query-builder-v5/#time-aggregation-windows"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}

View File

@@ -676,7 +676,7 @@ function QueryAggregationSelect({
</span>
<br />
<a
href="https://signoz.io/docs/querying/aggregation-grouping/#core-aggregation-functions-logs--traces"
href="https://signoz.io/docs/userguide/query-builder-v5/#core-aggregation-functions"
target="_blank"
rel="noopener noreferrer"
style={{ color: '#1890ff', textDecoration: 'underline' }}

View File

@@ -1,7 +1,7 @@
.add-trace-operator-button,
.add-new-query-button,
.add-formula-button {
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
background: var(--query-builder-v2-background-color, var(--l2-background));
border: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}

View File

@@ -44,7 +44,7 @@ function TraceOperatorSection({
<div style={{ textAlign: 'center' }}>
Add Trace Matching
<Typography.Link
href="https://signoz.io/docs/querying/multi-query-analysis/#trace-matching"
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-trace-operators"
target="_blank"
style={{ textDecoration: 'underline' }}
>
@@ -106,7 +106,7 @@ export default function QueryFooter({
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/querying/multi-query-analysis/#advanced-comparisons"
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
target="_blank"
style={{ textDecoration: 'underline' }}
>

View File

@@ -40,14 +40,11 @@ $max-recents-shown: 5;
.query-status-container {
width: 32px;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
background-color: var(--l1-background) !important;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
border-radius: 2px;
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
@@ -86,16 +83,16 @@ $max-recents-shown: 5;
.cm-content {
border-radius: 2px;
border: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border: 1px solid var(--l1-border);
padding: 0px !important;
&:focus-within {
border-color: var(--query-builder-v2-border-color, var(--l2-border));
border-color: var(--l1-border);
}
}
&.cm-focused {
outline: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
outline: 1px solid var(--l1-border);
}
.cm-tooltip-autocomplete {
@@ -186,17 +183,11 @@ $max-recents-shown: 5;
font-family: 'Space Mono', monospace !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
color: var(--query-builder-v2-color, var(--l2-foreground)) !important;
background-color: var(--l1-background) !important;
color: var(--l2-foreground) !important;
&:hover {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
}
.cm-completionIcon {
@@ -214,10 +205,7 @@ $max-recents-shown: 5;
}
&[aria-selected='true'] {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
font-weight: 600 !important;
}
}
@@ -286,49 +274,25 @@ $max-recents-shown: 5;
}
.cm-line {
line-height: 36px !important;
line-height: 34px !important;
font-family: 'Space Mono', monospace !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
&,
.ͼ1a {
color: var(--query-builder-v2-color, var(--l2-foreground));
}
background-color: var(--l2-background) !important;
::-moz-selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
::selection {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
}
.cm-selectionBackground {
background: var(
--query-builder-v2-selection-background-color,
var(--l3-background)
) !important;
background: var(--l3-background) !important;
opacity: 0.5 !important;
}
.cm-placeholder {
color: var(
--query-builder-v2-placeholder-color,
var(--l3-foreground)
) !important;
}
}
.cursor-position {

View File

@@ -65,14 +65,6 @@
display: flex;
flex-direction: column;
gap: 8px;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
--input-border-color: var(--l2-border);
--input-hover-border-color: var(--internal-ant-border-color-hover);
--input-focus-border-color: var(--internal-ant-border-color-hover);
}
&-aggregation-container {

View File

@@ -1,11 +1,10 @@
import { ComponentType } from 'react';
import { TabsProps } from 'antd';
import { History } from 'history';
export type TabRoutes = {
name: React.ReactNode;
route: string;
Component: ComponentType;
Component: () => JSX.Element;
key: string;
};

View File

@@ -1,34 +0,0 @@
import { type MouseEvent, type ReactNode } from 'react';
import { Badge } from '@signozhq/ui/badge';
interface TagBadgeProps {
children: ReactNode;
// Show a remove button (editable contexts: create modal, settings drawer).
closable?: boolean;
onClose?: (event: MouseEvent<HTMLButtonElement>) => void;
className?: string;
}
// The single sienna tag chip used everywhere dashboards render tags — list rows,
// the details header, and the tag editors. Kept as one component so the tag
// styling stays identical across all of them.
function TagBadge({
children,
closable,
onClose,
className,
}: TagBadgeProps): JSX.Element {
return (
<Badge
color="sienna"
variant="outline"
className={className}
closable={closable}
onClose={onClose}
>
{children}
</Badge>
);
}
export default TagBadge;

View File

@@ -15,28 +15,59 @@
border: 1px solid var(--l2-border);
}
// Sienna chip via the @signozhq Badge; this only constrains its width.
// Sienna chip — matches the dashboard list-row tag badge.
.tag {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 240px;
height: 24px;
padding: 2px 4px 2px 8px;
border-radius: 50px;
border: 1px solid color-mix(in srgb, var(--bg-sienna-500) 20%, transparent);
background: color-mix(in srgb, var(--bg-sienna-500) 10%, transparent);
color: var(--bg-sienna-400);
font-size: 13px;
font-weight: var(--font-weight-normal);
line-height: 20px;
cursor: text;
}
// The tag label is a bare, chrome-less button inside the Badge (double-click to
// edit); strip its Button styling and let it ellipsize.
.tagLabel {
--button-height: auto;
--button-padding: 0;
--button-gap: 0;
--button-variant-ghost-background-color: transparent;
--button-variant-ghost-hover-background-color: transparent;
--button-variant-ghost-color: currentColor;
--button-variant-ghost-hover-color: currentColor;
--button-variant-ghost-color: inherit;
--button-variant-ghost-hover-color: inherit;
overflow: hidden;
max-width: 200px;
font-size: 13px;
font-weight: var(--font-weight-normal);
text-overflow: ellipsis;
white-space: nowrap;
cursor: text;
}
.remove {
// Size overrides to fit the chip, plus a sienna-tinted hover — the Button's
// default ghost hover is a grey that clashes with the chip. Resting color is
// left at the Button default.
--button-height: 16px;
--button-padding: 0;
--button-border-radius: 50%;
--button-variant-ghost-hover-background-color: color-mix(
in srgb,
var(--bg-sienna-500) 22%,
transparent
);
--button-variant-ghost-hover-color: var(--bg-sienna-400);
width: 16px;
min-width: 16px;
flex: none;
}
.input {
flex: 1;
min-width: 120px;

View File

@@ -2,9 +2,9 @@ import { type ChangeEvent, type KeyboardEvent, useState } from 'react';
import { Button } from '@signozhq/ui/button';
import { Input } from '@signozhq/ui/input';
import { Typography } from '@signozhq/ui/typography';
import { X } from '@signozhq/icons';
import cx from 'classnames';
import TagBadge from '../TagBadge/TagBadge';
import { parseKeyValueTag } from './utils';
import styles from './TagKeyValueInput.module.scss';
@@ -120,23 +120,27 @@ function TagKeyValueInput({
onBlur={commitEdit}
/>
) : (
<TagBadge
key={tag}
className={styles.tag}
closable
onClose={(): void => removeTag(tag)}
>
<div key={tag} className={styles.tag} data-testid={`${testId}-chip`}>
<Button
variant="ghost"
color="secondary"
className={styles.tagLabel}
title="Double-click to edit"
testId={`${testId}-chip`}
onDoubleClick={(): void => startEdit(index)}
>
{tag}
</Button>
</TagBadge>
<Button
variant="ghost"
color="secondary"
size="icon"
className={styles.remove}
aria-label={`Remove ${tag}`}
onClick={(): void => removeTag(tag)}
>
<X size={12} />
</Button>
</div>
),
)}
<Input

View File

@@ -1,5 +1,5 @@
export const apDexToolTipText =
"Apdex is a way to measure your users' satisfaction with the response time of your web service. It's represented as a score from 0-1.";
export const apDexToolTipUrl =
'https://signoz.io/docs/alerts-management/apdex-alerts/?utm_source=product&utm_medium=frontend&utm_campaign=apdex';
'https://signoz.io/docs/userguide/metrics/#apdex?utm_source=product&utm_medium=frontend&utm_campaign=apdex';
export const apDexToolTipUrlText = 'Learn more about Apdex.';

View File

@@ -68,7 +68,7 @@ function AlertChannels(): JSX.Element {
<RightActionContainer>
<TextToolTip
text={t('tooltip_notification_channels')}
url="https://signoz.io/docs/setup-alerts-notification/"
url="https://signoz.io/docs/userguide/alerts-management/#setting-notification-channel"
/>
<Tooltip

View File

@@ -29,7 +29,8 @@ function SelectAlertType({ onSelect }: SelectAlertTypeProps): JSX.Element {
let url = '';
switch (option) {
case AlertTypes.ANOMALY_BASED_ALERT:
url = 'https://signoz.io/docs/alerts-management/anomaly-based-alerts/';
url =
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples';
break;
case AlertTypes.METRICS_BASED_ALERT:
url =

View File

@@ -31,7 +31,8 @@ export const ALERT_TYPE_URL_MAP: Record<
'https://signoz.io/docs/alerts-management/exceptions-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
},
[AlertTypes.ANOMALY_BASED_ALERT]: {
selection: 'https://signoz.io/docs/alerts-management/anomaly-based-alerts/',
selection:
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-source-selection-page#examples',
creation:
'https://signoz.io/docs/alerts-management/anomaly-based-alerts/?utm_source=product&utm_medium=alert-creation-page',
},

View File

@@ -1,4 +1,3 @@
import type { MouseEvent as ReactMouseEvent } from 'react';
import type { PrecisionOption } from 'components/Graph/types';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
@@ -28,7 +27,7 @@ interface PieArcProps {
fill: string;
onEnter: (slice: PieSlice, centroidX: number, centroidY: number) => void;
onLeave: () => void;
onClick?: (slice: PieSlice, event: ReactMouseEvent) => void;
onClick?: (slice: PieSlice) => void;
}
/**
@@ -73,7 +72,7 @@ export default function PieArc({
<g
onMouseEnter={(): void => onEnter(slice, centroidX, centroidY)}
onMouseLeave={onLeave}
onClick={(event): void => onClick?.(slice, event)}
onClick={(): void => onClick?.(slice)}
>
<path d={arcPath} fill={fill} />
{shouldShowLabel && (

View File

@@ -80,7 +80,6 @@ describe('PieArc', () => {
expect(onLeave).toHaveBeenCalledTimes(1);
fireEvent.click(g);
// onClick now also receives the DOM event (for drill-down popover positioning).
expect(onClick).toHaveBeenCalledWith(SLICE, expect.anything());
expect(onClick).toHaveBeenCalledWith(SLICE);
});
});

View File

@@ -1,4 +1,3 @@
import type { MouseEvent as ReactMouseEvent } from 'react';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import { PrecisionOption } from 'components/Graph/types';
import {
@@ -80,10 +79,6 @@ export interface PieSlice {
label: string;
value: number;
color: string;
/** Source query of the slice's value column — the drill-down target (present for V2 panels). */
queryName?: string;
/** Group-by key→value of the slice's source row, used to build drill-down filters. */
labels?: Record<string, string>;
}
/**
@@ -104,7 +99,7 @@ export interface PieChartProps {
* (shared GRAPH_VISIBILITY_STATES, keyed by label). Omit to disable persistence.
*/
id?: string;
/** Fired when a slice's arc is clicked; carries the DOM event for popover positioning. */
onSliceClick?: (slice: PieSlice, event: ReactMouseEvent) => void;
/** Fired when a slice (or its legend entry) is clicked. */
onSliceClick?: (slice: PieSlice) => void;
'data-testid'?: string;
}

View File

@@ -3,6 +3,7 @@ import { Input } from '@signozhq/ui/input';
import { Button } from 'antd';
import { PrecisionOption, PrecisionOptionsEnum } from 'components/Graph/types';
import { ResizeTable } from 'components/ResizeTable';
import { useNotifications } from 'hooks/useNotifications';
import { UPlotConfigBuilder } from 'lib/uPlotV2/config/UPlotConfigBuilder';
import { usePlotContext } from 'lib/uPlotV2/context/PlotContext';
import useLegendsSync from 'lib/uPlotV2/hooks/useLegendsSync';
@@ -10,7 +11,6 @@ import {
selectIsDashboardLocked,
useDashboardStore,
} from 'providers/Dashboard/store/useDashboardStore';
import { toast } from '@signozhq/ui/sonner';
import { getChartManagerColumns } from './getChartMangerColumns';
import { ExtendedChartDataset, getDefaultTableDataSet } from './utils';
@@ -44,6 +44,7 @@ export default function ChartManager({
decimalPrecision = PrecisionOptionsEnum.TWO,
onCancel,
}: ChartManagerProps): JSX.Element {
const { notifications } = useNotifications();
const { legendItemsMap } = useLegendsSync({
config,
subscribeToFocusChange: false,
@@ -135,9 +136,11 @@ export default function ChartManager({
const handleSave = useCallback((): void => {
syncSeriesVisibilityToLocalStorage();
toast.success('The updated graphs & legends are saved');
notifications.success({
message: 'The updated graphs & legends are saved',
});
onCancel?.();
}, [syncSeriesVisibilityToLocalStorage, onCancel]);
}, [syncSeriesVisibilityToLocalStorage, notifications, onCancel]);
return (
<div className="chart-manager-container">

View File

@@ -5,7 +5,7 @@ import { render, screen } from 'tests/test-utils';
import ChartManager from '../ChartManager';
const mockSyncSeriesVisibilityToLocalStorage = jest.fn();
const mockToastSuccess = jest.fn();
const mockNotificationsSuccess = jest.fn();
jest.mock('lib/uPlotV2/context/PlotContext', () => ({
usePlotContext: (): {
@@ -46,11 +46,12 @@ jest.mock('providers/Dashboard/store/useDashboardStore', () => ({
}): boolean => s.dashboardData?.locked ?? false,
}));
jest.mock('@signozhq/ui/sonner', () => ({
...jest.requireActual('@signozhq/ui/sonner'),
toast: {
success: (...args: unknown[]): unknown => mockToastSuccess(...args),
},
jest.mock('hooks/useNotifications', () => ({
useNotifications: (): { notifications: { success: jest.Mock } } => ({
notifications: {
success: mockNotificationsSuccess,
},
}),
}));
jest.mock('components/ResizeTable', () => {
@@ -159,7 +160,7 @@ describe('ChartManager', () => {
expect(screen.queryByTestId('row-2')).not.toBeInTheDocument();
});
it('calls syncSeriesVisibilityToLocalStorage, toast.success, and onCancel when Save is clicked', async () => {
it('calls syncSeriesVisibilityToLocalStorage, notifications.success, and onCancel when Save is clicked', async () => {
render(
<ChartManager
config={createMockConfig() as UPlotConfigBuilder}
@@ -171,9 +172,9 @@ describe('ChartManager', () => {
await userEvent.click(screen.getByRole('button', { name: /Save/ }));
expect(mockSyncSeriesVisibilityToLocalStorage).toHaveBeenCalledTimes(1);
expect(mockToastSuccess).toHaveBeenCalledWith(
'The updated graphs & legends are saved',
);
expect(mockNotificationsSuccess).toHaveBeenCalledWith({
message: 'The updated graphs & legends are saved',
});
expect(mockOnCancel).toHaveBeenCalledTimes(1);
});
});

View File

@@ -2,17 +2,9 @@
position: relative;
display: flex;
width: 100%;
height: auto;
height: 100%;
flex-direction: column;
// Stacked children (the FullView / standalone graph-manager) sit below the chart
// in the same container; size the chart region to its content so they aren't
// pushed out. Only this case opts out of filling the height — the dashboard grid,
// alert preview, and other charts keep 100% so they fill their container.
&--with-layout-children {
height: auto;
}
&--legend-right {
flex-direction: row;
}

View File

@@ -63,7 +63,6 @@ export default function ChartLayout({
className={cx('chart-layout', {
'chart-layout--legend-right':
legendConfig.position === LegendPosition.RIGHT,
'chart-layout--with-layout-children': !!layoutChildren,
})}
>
<div className="chart-layout__content">

View File

@@ -717,13 +717,13 @@ function ExplorerOptions({
const infoIconLink = useMemo(() => {
if (isLogsExplorer) {
return 'https://signoz.io/docs/userguide/logs_query_builder/?utm_source=product&utm_medium=logs-explorer-toolbar';
return 'https://signoz.io/docs/product-features/logs-explorer/?utm_source=product&utm_medium=logs-explorer-toolbar';
}
// TODO: Add metrics explorer info icon link
if (isMetricsExplorer) {
return '';
}
return 'https://signoz.io/docs/userguide/traces/?utm_source=product&utm_medium=trace-explorer-toolbar';
return 'https://signoz.io/docs/product-features/trace-explorer/?utm_source=product&utm_medium=trace-explorer-toolbar';
}, [isLogsExplorer, isMetricsExplorer]);
const getQueryName = (query: Query): string => {

View File

@@ -53,26 +53,6 @@
}
.alert-query-section-container {
--query-builder-v2-color: var(--l2-foreground);
--query-builder-v2-background-color: var(--l3-background);
--query-builder-v2-border-color: var(--l3-border);
--query-builder-v2-selection-background-color: var(--l2-background);
--query-builder-v2-chip-decorator-background-color: var(--l2-background);
--query-builder-v2-placeholder-color: var(--l3-foreground);
--query-builder-v2-selected-background-color: var(--l3-foreground);
--query-search-background-color: var(--l3-background);
--query-search-background-color-selection: var(--l3-background);
--input-with-label-border-color: var(--l3-border);
--input-with-label-background-color: var(--l3-background);
--input-with-label-color: var(--l2-foreground);
--query-builder-v2-toggle-group-background-color: var(--l3-background);
--query-builder-v2-toggle-group-border-color: var(--l3-border);
--query-builder-v2-toggle-group-active-background-color: var(--l2-background);
--query-builder-v2-toggle-group-background-color-hover: var(--l3-background);
--input-height: 36px;
--input-hover-background: var(--l3-background);
--input-hover-border-color: var(--internal-ant-border-color-hover);
.ant-card-body {
padding: 0px;

View File

@@ -3,30 +3,6 @@
overflow-x: auto;
overflow-y: hidden;
--query-builder-v2-color: var(--l2-foreground);
--query-builder-v2-background-color: var(--l3-background);
--query-builder-v2-border-color: var(--l3-border);
--query-builder-v2-selection-background-color: var(--l2-background);
--query-builder-v2-chip-decorator-background-color: var(--l2-background);
--query-builder-v2-placeholder-color: var(--l3-foreground);
--query-builder-v2-selected-background-color: var(--l3-foreground);
--query-search-background-color: var(--l3-background);
--query-search-background-color-selection: var(--l3-background);
--input-with-label-border-color: var(--l3-border);
--input-with-label-background-color: var(--l3-background);
--input-with-label-color: var(--l2-foreground);
--query-builder-v2-toggle-group-background-color: var(--l3-background);
--query-builder-v2-toggle-group-border-color: var(--l3-border);
--query-builder-v2-toggle-group-active-background-color: var(--l2-background);
--query-builder-v2-toggle-group-background-color-hover: var(--l3-background);
--input-height: 36px;
--input-hover-background: var(--l3-background);
--input-hover-border-color: var(--internal-ant-border-color-hover);
--input-focus-border-color: var(--internal-ant-border-color-hover);
--input-background: var(--l3-background);
--input-focus-background: var(--l3-background);
--input-border-color: var(--l3-border);
.full-view-header-container {
display: flex;
flex-direction: column;

View File

@@ -201,7 +201,7 @@ export default function SavedViews({
});
window.open(
'https://signoz.io/docs/metrics-management/metrics-explorer/#saved-views-in-metrics-explorer',
'https://signoz.io/docs/product-features/saved-view/',
'_blank',
'noopener noreferrer',
);

View File

@@ -29,12 +29,12 @@ export const checkListStepToPreferenceKeyMap = {
export const DOCS_LINKS = {
ADD_DATA_SOURCE: 'https://signoz.io/docs/instrumentation/overview/',
SEND_LOGS: 'https://signoz.io/docs/userguide/logs_query_builder/',
SEND_LOGS: 'https://signoz.io/docs/userguide/logs/',
SEND_TRACES: 'https://signoz.io/docs/userguide/traces/',
SEND_METRICS: 'https://signoz.io/docs/metrics-management/metrics-explorer/',
SETUP_ALERTS: 'https://signoz.io/docs/alerts/',
SETUP_ALERTS: 'https://signoz.io/docs/userguide/alerts-management/',
SETUP_SAVED_VIEWS:
'https://signoz.io/docs/metrics-management/metrics-explorer/#saved-views-in-metrics-explorer',
'https://signoz.io/docs/product-features/saved-view/#step-2-save-your-view',
SETUP_DASHBOARDS: 'https://signoz.io/docs/userguide/manage-dashboards/',
};

View File

@@ -82,7 +82,7 @@ export function K8sEmptyState({
<span className={styles.message}>
Please refer to{' '}
<a
href="https://signoz.io/docs/infrastructure-monitoring/hostmetrics/"
href="https://signoz.io/docs/userguide/hostmetrics/"
target="_blank"
rel="noreferrer"
>

View File

@@ -611,7 +611,7 @@ describe('K8sBaseList', () => {
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute(
'href',
'https://signoz.io/docs/infrastructure-monitoring/hostmetrics/',
'https://signoz.io/docs/userguide/hostmetrics/',
);
});
});

View File

@@ -66,6 +66,20 @@
background: colox-mix(var(--accent-primary), var(--bg-vanilla-100), 20%);
}
:global(.ant-collapse-header) {
border-bottom: 1px solid var(--l1-border);
padding: 12px 8px;
}
:global(.ant-collapse-content-box) {
padding: 0 !important;
padding-block: 0 !important;
:global(.quick-filters .checkbox-filter) {
padding-left: 18px;
}
}
:global(.quick-filters) {
overflow-y: auto;
overflow-x: hidden;
@@ -97,6 +111,22 @@
justify-content: space-between;
}
.quickFiltersCategoryLabel {
display: flex;
align-items: center;
gap: 4px;
}
.quickFiltersCategoryLabelIcon {
margin-right: 8px;
}
.quickFiltersCategoryLabelContainer {
display: flex;
align-items: center;
gap: 4px;
}
.listContainer {
flex: 1;
min-width: 0;
@@ -130,106 +160,3 @@
.listContainerFiltersVisible {
max-width: calc(100% - 280px);
}
.categorySelectorSection {
padding: var(--spacing-4);
}
.sectionHeader {
display: flex;
align-items: center;
gap: var(--spacing-4);
margin-bottom: var(--spacing-4);
&[data-type='filter'] {
padding: 0 var(--spacing-4);
}
}
.sectionLabel {
font-size: var(--periscope-font-size-small);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--l3-foreground);
white-space: nowrap;
}
.sectionLine {
flex: 1;
height: 1px;
background: var(--l1-border);
}
.categoryCard {
background: var(--l2-background);
border: 1px solid var(--l2-border);
border-radius: var(--spacing-3);
padding: var(--spacing-2);
}
.categoryList {
display: flex;
flex-direction: column;
gap: var(--spacing-1);
}
.categoryItem {
display: flex;
align-items: center;
gap: var(--spacing-5);
padding: var(--spacing-3) var(--spacing-4);
border: none;
border-radius: var(--spacing-2);
background: transparent;
cursor: pointer;
width: 100%;
text-align: left;
transition:
background-color 0.2s ease,
color 0.2s ease;
color: var(--l2-foreground);
font-size: var(--periscope-font-size-base);
--typography-color: var(--l1-foreground);
&:hover:not(.categoryItemSelected) {
background: var(--l2-background-hover);
}
svg {
color: var(--l3-foreground);
flex-shrink: 0;
transition: color 0.2s ease;
}
&.categoryItemSelected {
background: var(--accent-primary);
color: var(--l1-background);
font-weight: 600;
&:hover {
background: var(--accent-primary-hover, var(--accent-primary));
}
svg {
color: var(--l1-foreground);
}
}
}
.quickFiltersSection {
flex: 1;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0.1rem;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--accent-primary);
}
}

View File

@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import * as Sentry from '@sentry/react';
import { Button, Tooltip } from 'antd';
import { Button, CollapseProps } from 'antd';
import { Collapse, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import logEvent from 'api/common/logEvent';
import QuickFilters from 'components/QuickFilters/QuickFilters';
@@ -112,74 +113,192 @@ export default function InfraMonitoringK8s(): JSX.Element {
});
};
const categories = useMemo(
() => [
{
key: K8sCategories.PODS,
label: 'Pods',
icon: <Container size={14} />,
config: GetPodsQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.NODES,
label: 'Nodes',
icon: <Workflow size={14} />,
config: GetNodesQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.NAMESPACES,
label: 'Namespaces',
icon: <FilePenLine size={14} />,
config: GetNamespaceQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.CLUSTERS,
label: 'Clusters',
icon: <Boxes size={14} />,
config: GetClustersQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.DEPLOYMENTS,
label: 'Deployments',
icon: <Computer size={14} />,
config: GetDeploymentsQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.JOBS,
label: 'Jobs',
icon: <Bolt size={14} />,
config: GetJobsQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.DAEMONSETS,
label: 'DaemonSets',
icon: <Group size={14} />,
config: GetDaemonsetsQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.STATEFULSETS,
label: 'StatefulSets',
icon: <ArrowUpDown size={14} />,
config: GetStatefulsetsQuickFiltersConfig(dotMetricsEnabled),
},
{
key: K8sCategories.VOLUMES,
label: 'Volumes',
icon: <HardDrive size={14} />,
config: GetVolumesQuickFiltersConfig(dotMetricsEnabled),
},
],
[dotMetricsEnabled],
const renderCategoryLabel = (
icon: JSX.Element,
label: string,
): JSX.Element => (
<div className={styles.quickFiltersCategoryLabel}>
<div className={styles.quickFiltersCategoryLabelContainer}>
{icon}
<Typography.Text>{label}</Typography.Text>
</div>
</div>
);
const selectedCategoryConfig = useMemo(
() => categories.find((cat) => cat.key === selectedCategory)?.config,
[categories, selectedCategory],
);
const items: CollapseProps['items'] = [
{
label: renderCategoryLabel(
<Container size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Pods',
),
key: K8sCategories.PODS,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetPodsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<Workflow size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Nodes',
),
key: K8sCategories.NODES,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetNodesQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<FilePenLine size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Namespaces',
),
key: K8sCategories.NAMESPACES,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetNamespaceQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<Boxes size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Clusters',
),
key: K8sCategories.CLUSTERS,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetClustersQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<Computer size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Deployments',
),
key: K8sCategories.DEPLOYMENTS,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetDeploymentsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<Bolt size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Jobs',
),
key: K8sCategories.JOBS,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetJobsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<Group size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'DaemonSets',
),
key: K8sCategories.DAEMONSETS,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetDaemonsetsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<ArrowUpDown size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'StatefulSets',
),
key: K8sCategories.STATEFULSETS,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetStatefulsetsQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
{
label: renderCategoryLabel(
<HardDrive size={14} className={styles.quickFiltersCategoryLabelIcon} />,
'Volumes',
),
key: K8sCategories.VOLUMES,
showArrow: false,
children: (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={GetVolumesQuickFiltersConfig(dotMetricsEnabled)}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
/>
),
},
// TODO: Enable once we have implemented containers.
// {
// label: (
// <div className="k8s-quick-filters-category-label">
// <div className="k8s-quick-filters-category-label-container">
// <PackageOpen
// size={14}
// className="k8s-quick-filters-category-label-icon"
// />
// <Typography.Text>Containers</Typography.Text>
// </div>
// </div>
// ),
// key: K8sCategories.CONTAINERS,
// showArrow: false,
// children: (
// <QuickFilters
// source={QuickFiltersSource.INFRA_MONITORING}
// config={ContainersQuickFiltersConfig}
// handleFilterVisibilityChange={handleFilterVisibilityChange}
// onFilterChange={handleFilterChange}
// />
// ),
// },
];
const handleCategorySelect = (key: string): void => {
if (key !== selectedCategory) {
setSelectedCategory(key);
const handleCategoryChange = (key: string | string[]): void => {
if (Array.isArray(key) && key.length > 0) {
setSelectedCategory(key[0] as string);
// Reset filters
setUrlFilters(null);
setOrderBy(null);
@@ -213,58 +332,26 @@ export default function InfraMonitoringK8s(): JSX.Element {
<div className={styles.infraContentRow}>
{showFilters && (
<div className={styles.quickFiltersContainer}>
<div className={styles.categorySelectorSection}>
<div className={styles.sectionHeader} data-type="resource">
<Typography.Text className={styles.sectionLabel}>
Viewing · Resource
</Typography.Text>
<div className={styles.sectionLine} />
<Tooltip title="Collapse Filters">
<ArrowUpToLine
style={{ transform: 'rotate(270deg)' }}
onClick={handleFilterVisibilityChange}
size="md"
/>
</Tooltip>
</div>
<div className={styles.categoryCard}>
<div className={styles.categoryList}>
{categories.map((category) => (
<button
key={category.key}
type="button"
className={`${styles.categoryItem} ${
selectedCategory === category.key
? styles.categoryItemSelected
: ''
}`}
onClick={(): void => handleCategorySelect(category.key)}
data-testid={`category-${category.key}`}
>
{category.icon}
<Typography.Text>{category.label}</Typography.Text>
</button>
))}
</div>
</div>
</div>
<div className={styles.quickFiltersContainerHeader}>
<Typography.Text>Filters</Typography.Text>
<div className={styles.quickFiltersSection}>
<div className={styles.sectionHeader} data-type="filter">
<Typography.Text className={styles.sectionLabel}>
Filter by
</Typography.Text>
<div className={styles.sectionLine} />
</div>
{selectedCategoryConfig && (
<QuickFilters
source={QuickFiltersSource.INFRA_MONITORING}
config={selectedCategoryConfig}
handleFilterVisibilityChange={handleFilterVisibilityChange}
onFilterChange={handleFilterChange}
<Tooltip title="Collapse Filters">
<ArrowUpToLine
style={{ transform: 'rotate(270deg)' }}
onClick={handleFilterVisibilityChange}
size="md"
/>
)}
</Tooltip>
</div>
<Collapse
onChange={handleCategoryChange}
items={items}
defaultActiveKey={[selectedCategory]}
activeKey={[selectedCategory]}
accordion
bordered={false}
ghost
/>
</div>
)}

View File

@@ -1,228 +0,0 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { ChangeEvent, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import ApacheIcon from 'assets/CustomIcons/ApacheIcon';
import DockerIcon from 'assets/CustomIcons/DockerIcon';
import ElasticSearchIcon from 'assets/CustomIcons/ElasticSearchIcon';
import HerokuIcon from 'assets/CustomIcons/HerokuIcon';
import KubernetesIcon from 'assets/CustomIcons/KubernetesIcon';
import MongoDBIcon from 'assets/CustomIcons/MongoDBIcon';
import MySQLIcon from 'assets/CustomIcons/MySQLIcon';
import NginxIcon from 'assets/CustomIcons/NginxIcon';
import PostgreSQLIcon from 'assets/CustomIcons/PostgreSQLIcon';
import RedisIcon from 'assets/CustomIcons/RedisIcon';
import cx from 'classnames';
import {
ConciergeBell,
DraftingCompass,
Drill,
Plus,
X,
} from '@signozhq/icons';
import { DashboardTemplate } from 'types/api/dashboard/getAll';
import blankDashboardTemplatePreviewUrl from '@/assets/Images/blankDashboardTemplatePreview.svg';
import redisTemplatePreviewUrl from '@/assets/Images/redisTemplatePreview.svg';
import { filterTemplates } from '../utils';
import './DashboardTemplatesModal.styles.scss';
const templatesList: DashboardTemplate[] = [
{
name: 'Blank dashboard',
icon: <Drill />,
id: 'blank',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Alert Manager',
icon: <ConciergeBell />,
id: 'alertManager',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Apache',
icon: <ApacheIcon />,
id: 'apache',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Docker',
icon: <DockerIcon />,
id: 'docker',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Elasticsearch',
icon: <ElasticSearchIcon />,
id: 'elasticSearch',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'MongoDB',
icon: <MongoDBIcon />,
id: 'mongoDB',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Heroku',
icon: <HerokuIcon />,
id: 'heroku',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Nginx',
icon: <NginxIcon />,
id: 'nginx',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Kubernetes',
icon: <KubernetesIcon />,
id: 'kubernetes',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'MySQL',
icon: <MySQLIcon />,
id: 'mySQL',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'PostgreSQL',
icon: <PostgreSQLIcon />,
id: 'postgreSQL',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Redis',
icon: <RedisIcon />,
id: 'redis',
description: 'Create a custom dashboard from scratch.',
previewImage: redisTemplatePreviewUrl,
},
{
name: 'AWS',
icon: <DraftingCompass size={14} />,
id: 'aws',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
];
interface DashboardTemplatesContentProps {
onCreateNewDashboard: () => void;
/** When provided, renders the modal-style header with a close affordance. Omitted for inline use. */
onCancel?: () => void;
}
// The template gallery (search + list + preview + create), extracted from the
// modal so it can be embedded inline (e.g. the V2 new-dashboard modal's template
// tab) as well as inside DashboardTemplatesModal. Styles live under the global
// `.new-dashboard-templates-modal` scope, so inline callers wrap it in that class.
export default function DashboardTemplatesContent({
onCreateNewDashboard,
onCancel,
}: DashboardTemplatesContentProps): JSX.Element {
const [selectedDashboardTemplate, setSelectedDashboardTemplate] = useState(
templatesList[0],
);
const [dashboardTemplates, setDashboardTemplates] = useState(templatesList);
const handleDashboardTemplateSearch = (
event: ChangeEvent<HTMLInputElement>,
) => {
const searchText = event.target.value;
const filteredTemplates = filterTemplates(searchText, templatesList);
setDashboardTemplates(filteredTemplates);
};
return (
<div className="new-dashboard-templates-content-container">
{onCancel && (
<div className="new-dashboard-templates-content-header">
<Typography.Text>New Dashboard</Typography.Text>
<X size={14} className="periscope-btn ghost" onClick={onCancel} />
</div>
)}
<div className="new-dashboard-templates-content">
<div className="new-dashboard-templates-list">
<Input
className="new-dashboard-templates-search"
placeholder="🔍 Search..."
onChange={handleDashboardTemplateSearch}
/>
<div className="templates-list">
{dashboardTemplates.map((template) => (
<div
className={cx(
'template-list-item',
selectedDashboardTemplate.id === template.id ? 'active' : '',
)}
key={template.name}
onClick={() => setSelectedDashboardTemplate(template)}
>
<div className="template-icon">{template.icon}</div>
<div className="template-name">{template.name}</div>
</div>
))}
</div>
</div>
<div className="new-dashboard-template-preview">
<div className="template-preview-header">
<div className="template-preview-title">
<div className="template-preview-icon">
{selectedDashboardTemplate.icon}
</div>
<div className="template-info">
<div className="template-name">{selectedDashboardTemplate.name}</div>
<div className="template-description">
{selectedDashboardTemplate.description}
</div>
</div>
</div>
<div className="create-dashboard-btn">
<Button
type="primary"
className="periscope-btn primary"
icon={<Plus size={14} />}
onClick={onCreateNewDashboard}
>
New dashboard
</Button>
</div>
</div>
<div className="template-preview-image">
<img
src={selectedDashboardTemplate.previewImage}
alt={`${selectedDashboardTemplate.name}-preview`}
/>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,9 +1,129 @@
import { Modal } from 'antd';
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { ChangeEvent, useState } from 'react';
import { Input } from '@signozhq/ui/input';
import { Button, Modal } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import ApacheIcon from 'assets/CustomIcons/ApacheIcon';
import DockerIcon from 'assets/CustomIcons/DockerIcon';
import ElasticSearchIcon from 'assets/CustomIcons/ElasticSearchIcon';
import HerokuIcon from 'assets/CustomIcons/HerokuIcon';
import KubernetesIcon from 'assets/CustomIcons/KubernetesIcon';
import MongoDBIcon from 'assets/CustomIcons/MongoDBIcon';
import MySQLIcon from 'assets/CustomIcons/MySQLIcon';
import NginxIcon from 'assets/CustomIcons/NginxIcon';
import PostgreSQLIcon from 'assets/CustomIcons/PostgreSQLIcon';
import RedisIcon from 'assets/CustomIcons/RedisIcon';
import cx from 'classnames';
import {
ConciergeBell,
DraftingCompass,
Drill,
Plus,
X,
} from '@signozhq/icons';
import { DashboardTemplate } from 'types/api/dashboard/getAll';
import DashboardTemplatesContent from './DashboardTemplatesContent';
import blankDashboardTemplatePreviewUrl from '@/assets/Images/blankDashboardTemplatePreview.svg';
import redisTemplatePreviewUrl from '@/assets/Images/redisTemplatePreview.svg';
import { filterTemplates } from '../utils';
import './DashboardTemplatesModal.styles.scss';
const templatesList: DashboardTemplate[] = [
{
name: 'Blank dashboard',
icon: <Drill />,
id: 'blank',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Alert Manager',
icon: <ConciergeBell />,
id: 'alertManager',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Apache',
icon: <ApacheIcon />,
id: 'apache',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Docker',
icon: <DockerIcon />,
id: 'docker',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Elasticsearch',
icon: <ElasticSearchIcon />,
id: 'elasticSearch',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'MongoDB',
icon: <MongoDBIcon />,
id: 'mongoDB',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Heroku',
icon: <HerokuIcon />,
id: 'heroku',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Nginx',
icon: <NginxIcon />,
id: 'nginx',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Kubernetes',
icon: <KubernetesIcon />,
id: 'kubernetes',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'MySQL',
icon: <MySQLIcon />,
id: 'mySQL',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'PostgreSQL',
icon: <PostgreSQLIcon />,
id: 'postgreSQL',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
{
name: 'Redis',
icon: <RedisIcon />,
id: 'redis',
description: 'Create a custom dashboard from scratch.',
previewImage: redisTemplatePreviewUrl,
},
{
name: 'AWS',
icon: <DraftingCompass size={14} />,
id: 'aws',
description: 'Create a custom dashboard from scratch.',
previewImage: blankDashboardTemplatePreviewUrl,
},
];
interface DashboardTemplatesModalProps {
showNewDashboardTemplatesModal: boolean;
onCreateNewDashboard: () => void;
@@ -15,6 +135,20 @@ export default function DashboardTemplatesModal({
onCreateNewDashboard,
onCancel,
}: DashboardTemplatesModalProps): JSX.Element {
const [selectedDashboardTemplate, setSelectedDashboardTemplate] = useState(
templatesList[0],
);
const [dashboardTemplates, setDashboardTemplates] = useState(templatesList);
const handleDashboardTemplateSearch = (
event: ChangeEvent<HTMLInputElement>,
) => {
const searchText = event.target.value;
const filteredTemplates = filterTemplates(searchText, templatesList);
setDashboardTemplates(filteredTemplates);
};
return (
<Modal
wrapClassName="new-dashboard-templates-modal"
@@ -25,10 +159,75 @@ export default function DashboardTemplatesModal({
destroyOnClose
width="60vw"
>
<DashboardTemplatesContent
onCreateNewDashboard={onCreateNewDashboard}
onCancel={onCancel}
/>
<div className="new-dashboard-templates-content-container">
<div className="new-dashboard-templates-content-header">
<Typography.Text>New Dashboard</Typography.Text>
<X size={14} className="periscope-btn ghost" onClick={onCancel} />
</div>
<div className="new-dashboard-templates-content">
<div className="new-dashboard-templates-list">
<Input
className="new-dashboard-templates-search"
placeholder="🔍 Search..."
onChange={handleDashboardTemplateSearch}
/>
<div className="templates-list">
{dashboardTemplates.map((template) => (
<div
className={cx(
'template-list-item',
selectedDashboardTemplate.id === template.id ? 'active' : '',
)}
key={template.name}
onClick={() => setSelectedDashboardTemplate(template)}
>
<div className="template-icon">{template.icon}</div>
<div className="template-name">{template.name}</div>
</div>
))}
</div>
</div>
<div className="new-dashboard-template-preview">
<div className="template-preview-header">
<div className="template-preview-title">
<div className="template-preview-icon">
{selectedDashboardTemplate.icon}
</div>
<div className="template-info">
<div className="template-name">{selectedDashboardTemplate.name}</div>
<div className="template-description">
{selectedDashboardTemplate.description}
</div>
</div>
</div>
<div className="create-dashboard-btn">
<Button
type="primary"
className="periscope-btn primary"
icon={<Plus size={14} />}
onClick={onCreateNewDashboard}
>
New dashboard
</Button>
</div>
</div>
<div className="template-preview-image">
<img
src={selectedDashboardTemplate.previewImage}
alt={`${selectedDashboardTemplate.name}-preview`}
/>
</div>
</div>
</div>
</div>
</Modal>
);
}

View File

@@ -164,7 +164,7 @@ function BreakDown(): JSX.Element {
Meter metrics data is aggregated over 1 hour period. Please select time
range accordingly.&nbsp;
<a
href="https://signoz.io/docs/cost-meter/overview/#get-started"
href="https://signoz.io/docs/cost-meter/overview/#accessing-cost-meter"
rel="noopener noreferrer"
target="_blank"
style={{ textDecoration: 'underline' }}

View File

@@ -14,14 +14,6 @@
.meter-explorer-content-section {
width: 100%;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
--input-border-color: var(--l2-border);
--input-hover-border-color: var(--internal-ant-border-color-hover);
--input-focus-border-color: var(--internal-ant-border-color-hover);
.explore-header {
display: flex;
align-items: center;

View File

@@ -197,7 +197,7 @@ function TopOperationsTable({
const entryPointSpanInfo = {
text: 'Shows the spans where requests enter new services for the first time',
url: 'https://signoz.io/docs/apm-and-distributed-tracing/application-details/',
url: 'https://signoz.io/docs/traces-management/guides/entry-point-spans-service-overview/',
urlText: 'Learn more about Entrypoint Spans.',
};

View File

@@ -1,14 +1,6 @@
.metrics-explorer-explore-container {
padding-bottom: 80px;
// Meant to fix the query builder colors
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
--input-border-color: var(--l2-border);
--input-hover-border-color: var(--internal-ant-border-color-hover);
--input-focus-border-color: var(--internal-ant-border-color-hover);
.explore-header {
display: flex;
align-items: center;

View File

@@ -1,12 +1,4 @@
.dashboard-navigation {
// Meant to fix the query builder colors
--input-background: var(--l2-background);
--input-hover-background: var(--l2-background);
--input-focus-background: var(--l2-background);
--input-border-color: var(--l2-border);
--input-hover-border-color: var(--internal-ant-border-color-hover);
--input-focus-border-color: var(--internal-ant-border-color-hover);
.run-query-dashboard-btn {
min-width: 180px;
}

View File

@@ -64,7 +64,7 @@ function ConfigureGoogleAuthAuthnProvider({
Enter OAuth 2.0 credentials obtained from the Google API Console below.
Read the{' '}
<a
href="https://signoz.io/docs/manage/administrator-guide/sso/overview/"
href="https://signoz.io/docs/userguide/sso-authentication"
target="_blank"
rel="noreferrer"
>

View File

@@ -38,7 +38,7 @@ function ConfigureOIDCAuthnProvider({
Configure OpenID Connect Single Sign-On with your Identity Provider. Read
the{' '}
<a
href="https://signoz.io/docs/manage/administrator-guide/sso/overview/"
href="https://signoz.io/docs/userguide/sso-authentication"
target="_blank"
rel="noreferrer"
>

View File

@@ -37,7 +37,7 @@ function ConfigureSAMLAuthnProvider({
<p className="authn-provider__description">
Configure SAML 2.0 Single Sign-On with your Identity Provider. Read the{' '}
<a
href="https://signoz.io/docs/manage/administrator-guide/sso/overview/"
href="https://signoz.io/docs/userguide/sso-authentication"
target="_blank"
rel="noreferrer"
>

View File

@@ -31,14 +31,8 @@
min-width: 32px;
border-radius: 2px;
--periscope-btn-border-color: var(
--query-builder-v2-border-color,
var(--l2-border)
);
--periscope-btn-background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
);
border: 1px solid var(--l1-border);
background: var(--l2-background);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
}

View File

@@ -27,11 +27,8 @@
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;
background-color: var(
--query-builder-v2-background-color,
var(--l2-background)
) !important;
margin-left: -1px;
background-color: var(--l1-border) !important;
opacity: 0.8;
&:disabled {
opacity: 0.4;
@@ -89,8 +86,8 @@
border-bottom-left-radius: 3px;
.ant-select-selector {
border: 1px solid var(--query-builder-v2-border-color, none);
background: var(--query-builder-v2-background-color, var(--l3-background));
border: none;
background: var(--l3-background);
}
&.showInput {
@@ -104,20 +101,12 @@
.query-function-value {
width: 70px;
border-left: 0;
background: var(--query-builder-v2-background-color, var(--l2-background));
background: var(--l2-background);
border-radius: 0;
border: 1px solid transparent;
border-top: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-bottom: 1px solid
var(--query-builder-v2-border-color, var(--l2-border));
height: 32px;
&:focus {
border-color: var(--internal-ant-border-color-focus);
}
&:hover {
border-color: var(--internal-ant-border-color-hover);
border-color: transparent !important;
}
}
@@ -125,7 +114,7 @@
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
border-left: 1px solid var(--query-builder-v2-border-color, var(--l2-border));
border-left: 1px solid var(--l2-border);
border-top-left-radius: 0px !important;
border-bottom-left-radius: 0px !important;

View File

@@ -216,7 +216,7 @@ export default function QueryFunctions({
Add new function
<Typography.Link
style={{ textDecoration: 'underline' }}
href="https://signoz.io/docs/querying/functions-extended-analysis/?utm_source=product&utm_medium=query-builder"
href="https://signoz.io/docs/userguide/query-builder/?utm_source=product&utm_medium=query-builder#functions-for-extended-data-analysis"
target="_blank"
>
{' '}

View File

@@ -100,7 +100,7 @@ function Version(): JSX.Element {
{!isError && !isLatestVersion && (
<div className="version-page-upgrade-container">
<Button
href="https://signoz.io/docs/opentelemetry-collection-agents/docker/overview/"
href="https://signoz.io/docs/operate/docker-standalone/#upgrade"
target="_blank"
type="primary"
className="periscope-btn primary"

View File

@@ -2,8 +2,8 @@ import { useCallback, useMemo } from 'react';
import { useQueryClient } from 'react-query';
import {
getGetServiceAccountRolesQueryKey,
useCreateServiceAccountRoleDeprecated,
useDeleteServiceAccountRoleDeprecated,
useCreateServiceAccountRole,
useDeleteServiceAccountRole,
useGetServiceAccountRoles,
} from 'api/generated/services/serviceaccount';
import type { AuthtypesRoleDTO } from 'api/generated/services/sigNoz.schemas';
@@ -46,10 +46,10 @@ export function useServiceAccountRoleManager(
);
// the retry for these mutations is safe due to being idempotent on backend
const { mutateAsync: createRole } = useCreateServiceAccountRoleDeprecated({
const { mutateAsync: createRole } = useCreateServiceAccountRole({
mutation: { retry: retryOn429 },
});
const { mutateAsync: deleteRole } = useDeleteServiceAccountRoleDeprecated({
const { mutateAsync: deleteRole } = useDeleteServiceAccountRole({
mutation: { retry: retryOn429 },
});

View File

@@ -1,21 +0,0 @@
# AuthZ
Permission-based authorization system for SigNoz frontend.
## Supported Resources
See [hooks/useAuthZ/permissions.type.ts](./hooks/useAuthZ/permissions.type.ts) for available resources and verbs.
If your page/content represents a resource not listed there, skip authz implementation — the backend doesn't enforce it yet.
## UI Gating
Need to gate UI based on permissions? See [components/README.md](./components/README.md).
Covers: AuthZButton, AuthZTooltip, withAuthZ*, AuthZGuard*, when to use each.
## Testing
Need to test authz behavior? See [utils/README.md](./utils/README.md).
Covers: MSW handlers, mock hooks, test patterns.

View File

@@ -1,81 +0,0 @@
import { ReactElement } from 'react';
import { render, screen } from 'tests/test-utils';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import type { AuthZObject } from 'lib/authz/hooks/useAuthZ/types';
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
import AuthZButton from './AuthZButton';
// AuthZButton is a thin composition over AuthZTooltip + Button. The denial
// tooltip / disabled-on-deny UX is owned and tested by AuthZTooltip; here we
// assert AuthZButton forwards the right props and renders a Button child.
jest.mock('lib/authz/components/AuthZTooltip/AuthZTooltip');
const mockTooltip = AuthZTooltip as unknown as jest.Mock;
const createPerm = buildPermission(
'create',
'serviceaccount:*' as AuthZObject<'create'>,
);
describe('AuthZButton', () => {
beforeEach(() => {
mockTooltip.mockImplementation(
({ children }: { children: ReactElement }) => children,
);
});
afterEach(() => {
mockTooltip.mockReset();
});
it('renders a Button child with forwarded props', () => {
render(
<AuthZButton checks={[createPerm]} testId="create-btn">
Create
</AuthZButton>,
);
expect(screen.getByTestId('create-btn')).toBeInTheDocument();
expect(screen.getByTestId('create-btn').tagName).toBe('BUTTON');
});
it('forwards checks and enables the check by default', () => {
render(
<AuthZButton checks={[createPerm]} testId="create-btn">
Create
</AuthZButton>,
);
expect(mockTooltip).toHaveBeenCalledTimes(1);
expect(mockTooltip.mock.calls[0][0]).toMatchObject({
checks: [createPerm],
enabled: true,
});
});
it('forwards a custom tooltipMessage', () => {
render(
<AuthZButton
checks={[createPerm]}
tooltipMessage="Ask an admin"
testId="create-btn"
>
Create
</AuthZButton>,
);
expect(mockTooltip.mock.calls[0][0]).toMatchObject({
tooltipMessage: 'Ask an admin',
});
});
it('passes authZEnabled through as the tooltip enabled flag', () => {
render(
<AuthZButton checks={[createPerm]} authZEnabled={false} testId="create-btn">
Create
</AuthZButton>,
);
expect(mockTooltip.mock.calls[0][0]).toMatchObject({ enabled: false });
});
});

View File

@@ -1,37 +0,0 @@
import { Button, ButtonProps } from '@signozhq/ui/button';
import AuthZTooltip from 'lib/authz/components/AuthZTooltip/AuthZTooltip';
import type { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
export type AuthZButtonProps = ButtonProps & {
/**
* Permissions required to enable the button (AND semantics).
*/
checks: BrandedPermission[];
/**
* Override the default denial tooltip message.
*/
tooltipMessage?: string;
/**
* Gate the permission check itself. When false, renders a plain button.
*/
authZEnabled?: boolean;
};
function AuthZButton({
checks,
tooltipMessage,
authZEnabled = true,
...buttonProps
}: AuthZButtonProps): JSX.Element {
return (
<AuthZTooltip
checks={checks}
enabled={authZEnabled}
tooltipMessage={tooltipMessage}
>
<Button {...buttonProps} />
</AuthZTooltip>
);
}
export default AuthZButton;

View File

@@ -1,202 +0,0 @@
import { render, screen, waitFor } from 'tests/test-utils';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import {
AUTHZ_CHECK_URL,
setupAuthzAllow,
setupAuthzDeny,
} from 'lib/authz/utils/authz-test-utils';
import type { AuthZObject } from 'lib/authz/hooks/useAuthZ/types';
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
import { AuthZGuard } from './AuthZGuard';
import { AuthZGuardContent } from './AuthZGuardContent';
import { AuthZGuardPage } from './AuthZGuardPage';
const readPerm = buildPermission('read', 'role:*' as AuthZObject<'read'>);
const Protected = (): JSX.Element => <div>Protected content</div>;
describe('AuthZGuard', () => {
it('renders children when allowed', async () => {
server.use(setupAuthzAllow(readPerm));
render(
<AuthZGuard checks={[readPerm]}>
<Protected />
</AuthZGuard>,
);
await waitFor(() => {
expect(screen.getByText('Protected content')).toBeInTheDocument();
});
});
it('renders the fallback when denied', async () => {
server.use(setupAuthzDeny(readPerm));
render(
<AuthZGuard checks={[readPerm]} fallback={<div>No access</div>}>
<Protected />
</AuthZGuard>,
);
await waitFor(() => {
expect(screen.getByText('No access')).toBeInTheDocument();
});
expect(screen.queryByText('Protected content')).not.toBeInTheDocument();
});
it('passes denied permissions to a function fallback', async () => {
server.use(setupAuthzDeny(readPerm));
render(
<AuthZGuard
checks={[readPerm]}
fallback={({ deniedPermissions }): JSX.Element => (
<div>denied: {deniedPermissions.length}</div>
)}
>
<Protected />
</AuthZGuard>,
);
await waitFor(() => {
expect(screen.getByText('denied: 1')).toBeInTheDocument();
});
});
it('renders nothing for a denied check with no fallback', async () => {
server.use(setupAuthzDeny(readPerm));
const { container } = render(
<AuthZGuard checks={[readPerm]}>
<Protected />
</AuthZGuard>,
);
await waitFor(() => {
expect(screen.queryByText('Protected content')).not.toBeInTheDocument();
});
expect(container).toBeEmptyDOMElement();
});
it('renders the loading fallback while checking', () => {
server.use(setupAuthzAllow(readPerm));
render(
<AuthZGuard checks={[readPerm]} fallbackOnLoading={<div>Loading</div>}>
<Protected />
</AuthZGuard>,
);
expect(screen.getByText('Loading…')).toBeInTheDocument();
});
it('fails open on error by default (renders children)', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) =>
res(ctx.status(500), ctx.json({ error: 'boom' })),
),
);
render(
<AuthZGuard checks={[readPerm]} fallback={<div>No access</div>}>
<Protected />
</AuthZGuard>,
);
await waitFor(() => {
expect(screen.getByText('Protected content')).toBeInTheDocument();
});
});
it('renders the fallback on error when failOpenOnError is false', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) =>
res(ctx.status(500), ctx.json({ error: 'boom' })),
),
);
render(
<AuthZGuard
checks={[readPerm]}
onFailRenderContent={false}
fallback={<div>No access</div>}
>
<Protected />
</AuthZGuard>,
);
await waitFor(() => {
expect(screen.getByText('No access')).toBeInTheDocument();
});
expect(screen.queryByText('Protected content')).not.toBeInTheDocument();
});
});
describe('AuthZGuardPage', () => {
it('renders the full-page denied screen when denied', async () => {
server.use(setupAuthzDeny(readPerm));
render(
<AuthZGuardPage checks={[readPerm]}>
<Protected />
</AuthZGuardPage>,
);
await waitFor(() => {
expect(
screen.getByText('Uh-oh! You are not authorized'),
).toBeInTheDocument();
});
expect(screen.getByText('read:role:*')).toBeInTheDocument();
});
it('renders the app loader while checking', () => {
server.use(setupAuthzDeny(readPerm));
render(
<AuthZGuardPage checks={[readPerm]}>
<Protected />
</AuthZGuardPage>,
);
expect(
screen.getByText(
'OpenTelemetry-Native Logs, Metrics and Traces in a single pane',
),
).toBeInTheDocument();
});
});
describe('AuthZGuardContent', () => {
it('renders the denied callout when denied', async () => {
server.use(setupAuthzDeny(readPerm));
render(
<AuthZGuardContent checks={[readPerm]}>
<Protected />
</AuthZGuardContent>,
);
await waitFor(() => {
expect(screen.getByText('read:role:*')).toBeInTheDocument();
});
expect(screen.queryByText('Protected content')).not.toBeInTheDocument();
});
it('renders children when allowed', async () => {
server.use(setupAuthzAllow(readPerm));
render(
<AuthZGuardContent checks={[readPerm]}>
<Protected />
</AuthZGuardContent>,
);
await waitFor(() => {
expect(screen.getByText('Protected content')).toBeInTheDocument();
});
});
});

View File

@@ -1,82 +0,0 @@
import { ReactElement, ReactNode, useMemo } from 'react';
import type { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
export type AuthZGuardFallback =
| ReactNode
| ((info: { deniedPermissions: BrandedPermission[] }) => ReactNode);
export type AuthZGuardProps = {
/**
* Permissions required to render `children` (AND semantics).
*/
checks: BrandedPermission[];
children: ReactElement;
/**
* Rendered when denied. A function receives the denied permissions.
*/
fallback?: AuthZGuardFallback;
fallbackOnLoading?: ReactNode;
/**
* By default, we don't expect the check API request to fail, in those cases, we prefer to show the content and then let the API fail (during list/create).
*
* In case you want to have a different behavior when request fail, set to false.
*
* @default true
*/
onFailRenderContent?: boolean;
};
function resolveFallback(
fallback: AuthZGuardFallback | undefined,
deniedPermissions: BrandedPermission[],
): ReactNode {
if (typeof fallback === 'function') {
return fallback({ deniedPermissions });
}
return fallback ?? null;
}
export function AuthZGuard({
checks,
children,
fallback,
fallbackOnLoading,
onFailRenderContent = true,
}: AuthZGuardProps): JSX.Element | null {
const { isLoading, error, permissions } = useAuthZ(checks);
// TODO(authz): Use allowed/deniedPermissions from useAuthZ after devtools PR merges
const { allowed, deniedPermissions } = useMemo(() => {
if (!permissions) {
return { allowed: false, deniedPermissions: [] as BrandedPermission[] };
}
const denied = Object.entries(permissions)
.filter(([, { isGranted }]) => !isGranted)
.map(([perm]) => perm as BrandedPermission);
return {
allowed: denied.length === 0,
deniedPermissions: denied,
};
}, [permissions]);
if (isLoading) {
return <>{fallbackOnLoading ?? null}</>;
}
if (error) {
return onFailRenderContent ? (
children
) : (
<>{resolveFallback(fallback, deniedPermissions)}</>
);
}
if (!allowed) {
return <>{resolveFallback(fallback, deniedPermissions)}</>;
}
return children;
}

View File

@@ -1,21 +0,0 @@
import { ReactElement } from 'react';
import PermissionDeniedCallout from 'lib/authz/components/PermissionDeniedCallout/PermissionDeniedCallout';
import { AuthZGuard, AuthZGuardProps } from './AuthZGuard';
export function AuthZGuardContent({
fallback,
...rest
}: AuthZGuardProps): JSX.Element | null {
return (
<AuthZGuard
{...rest}
fallback={
fallback ??
(({ deniedPermissions }): ReactElement => (
<PermissionDeniedCallout deniedPermissions={deniedPermissions} />
))
}
/>
);
}

View File

@@ -1,24 +0,0 @@
import { ReactElement } from 'react';
import AppLoading from 'components/AppLoading/AppLoading';
import PermissionDeniedFullPage from 'lib/authz/components/PermissionDeniedFullPage/PermissionDeniedFullPage';
import { AuthZGuard, AuthZGuardProps } from './AuthZGuard';
export function AuthZGuardPage({
fallback,
fallbackOnLoading,
...rest
}: AuthZGuardProps): JSX.Element | null {
return (
<AuthZGuard
{...rest}
fallbackOnLoading={fallbackOnLoading ?? <AppLoading />}
fallback={
fallback ??
(({ deniedPermissions }): ReactElement => (
<PermissionDeniedFullPage deniedPermissions={deniedPermissions} />
))
}
/>
);
}

View File

@@ -16,8 +16,6 @@ const noPermissions = {
isFetching: false,
error: null,
permissions: null,
allowed: false,
deniedPermissions: [] as BrandedPermission[],
refetchPermissions: jest.fn(),
};
@@ -162,11 +160,11 @@ describe('AuthZTooltip — multi-check (checks array)', () => {
</AuthZTooltip>,
);
const button = screen.getByRole('button', { name: 'Action' });
expect(button).toBeDisabled();
expect(screen.getByRole('button', { name: 'Action' })).toBeDisabled();
expect(button.getAttribute('data-denied-permissions')).toContain(sa);
expect(button.getAttribute('data-denied-permissions')).toContain(
const wrapper = screen.getByRole('button', { name: 'Action' }).parentElement;
expect(wrapper?.getAttribute('data-denied-permissions')).toContain(sa);
expect(wrapper?.getAttribute('data-denied-permissions')).toContain(
attachRolePerm,
);
});

View File

@@ -1,3 +1,7 @@
.wrapper {
cursor: not-allowed;
}
.errorContent {
background: var(--callout-error-background) !important;
border-color: var(--callout-error-border) !important;

View File

@@ -1,4 +1,4 @@
import { CSSProperties, ReactElement, cloneElement, useMemo } from 'react';
import { ReactElement, cloneElement, useMemo } from 'react';
import {
TooltipRoot,
TooltipContent,
@@ -11,13 +11,6 @@ import { formatPermission } from 'lib/authz/hooks/useAuthZ/utils';
import { useAppContext } from 'providers/App/App';
import styles from './AuthZTooltip.module.scss';
const DISABLED_STYLE: CSSProperties = {
pointerEvents: 'all',
cursor: 'not-allowed',
};
const noOp = (): void => {};
interface AuthZTooltipProps {
checks: BrandedPermission[];
children: ReactElement;
@@ -56,13 +49,11 @@ function AuthZTooltip({
}, [checks, permissions]);
if (shouldCheck && isLoading) {
return cloneElement(children, {
disabled: true,
style: DISABLED_STYLE,
onClick: noOp,
onMouseDown: noOp,
onPointerDown: noOp,
});
return (
<span className={styles.wrapper}>
{cloneElement(children, { disabled: true })}
</span>
);
}
if (!shouldCheck || deniedPermissions.length === 0) {
@@ -73,14 +64,12 @@ function AuthZTooltip({
<TooltipProvider>
<TooltipRoot>
<TooltipTrigger asChild>
{cloneElement(children, {
disabled: true,
style: DISABLED_STYLE,
onClick: noOp,
onMouseDown: noOp,
onPointerDown: noOp,
'data-denied-permissions': deniedPermissions.join(','),
})}
<span
className={styles.wrapper}
data-denied-permissions={deniedPermissions.join(',')}
>
{cloneElement(children, { disabled: true })}
</span>
</TooltipTrigger>
<TooltipContent className={styles.errorContent}>
{formatDeniedMessage(deniedPermissions, user.id, tooltipMessage)}

View File

@@ -0,0 +1,263 @@
import { ReactElement } from 'react';
import { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, waitFor } from 'tests/test-utils';
import {
AUTHZ_CHECK_URL,
authzMockResponse,
} from 'lib/authz/utils/authz-test-utils';
import { GuardAuthZ } from './GuardAuthZ';
describe('GuardAuthZ', () => {
const TestChild = (): ReactElement => <div>Protected Content</div>;
const LoadingFallback = (): ReactElement => <div>Loading...</div>;
const NoPermissionFallback = (_response: {
requiredPermissionName: BrandedPermission;
}): ReactElement => <div>Access denied</div>;
const NoPermissionFallbackWithSuggestions = (response: {
requiredPermissionName: BrandedPermission;
}): ReactElement => (
<div>
Access denied. Required permission: {response.requiredPermissionName}
</div>
);
it('should render children when permission is granted', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
render(
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
});
it('should render fallbackOnLoading when loading', () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (_req, res, ctx) => {
return res(
ctx.delay('infinite'),
ctx.status(200),
ctx.json({ data: [], status: 'success' }),
);
}),
);
render(
<GuardAuthZ
relation="read"
object="role:*"
fallbackOnLoading={<LoadingFallback />}
>
<TestChild />
</GuardAuthZ>,
);
expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render null when loading and no fallbackOnLoading provided', () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (_req, res, ctx) => {
return res(
ctx.delay('infinite'),
ctx.status(200),
ctx.json({ data: [], status: 'success' }),
);
}),
);
const { container } = render(
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
expect(container.firstChild).toBeNull();
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render children when API error occurs and no fallbackOnError provided (fail open)', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
render(
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
});
it('should render fallbackOnError when API error occurs and fallbackOnError is provided', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
render(
<GuardAuthZ
relation="read"
object="role:*"
fallbackOnError={<div>Custom error fallback</div>}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Custom error fallback')).toBeInTheDocument();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render fallbackOnNoPermissions when permission is denied', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
}),
);
render(
<GuardAuthZ
relation="update"
object="role:123"
fallbackOnNoPermissions={NoPermissionFallback}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Access denied')).toBeInTheDocument();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render null when permission is denied and no fallbackOnNoPermissions provided', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
}),
);
const { container } = render(
<GuardAuthZ relation="update" object="role:123">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should render null when permissions object is null', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(200), ctx.json({ data: [], status: 'success' }));
}),
);
const { container } = render(
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(container.firstChild).toBeNull();
});
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should pass requiredPermissionName to fallbackOnNoPermissions', async () => {
const permission = buildPermission('update', 'role:123');
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
}),
);
render(
<GuardAuthZ
relation="update"
object="role:123"
fallbackOnNoPermissions={NoPermissionFallbackWithSuggestions}
>
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(
screen.getByText(/Access denied. Required permission:/),
).toBeInTheDocument();
});
expect(
screen.getAllByText(
new RegExp(permission.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
).length,
).toBeGreaterThan(0);
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();
});
it('should handle different relation and object combinations', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const { rerender } = render(
<GuardAuthZ relation="read" object="role:*">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
rerender(
<GuardAuthZ relation="delete" object="role:456">
<TestChild />
</GuardAuthZ>,
);
await waitFor(() => {
expect(screen.getByText('Protected Content')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,50 @@
import { ReactElement } from 'react';
import {
AuthZObject,
AuthZRelation,
BrandedPermission,
} from 'lib/authz/hooks/useAuthZ/types';
import { useAuthZ } from 'lib/authz/hooks/useAuthZ/useAuthZ';
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
export type GuardAuthZProps<R extends AuthZRelation> = {
children: ReactElement;
relation: R;
object: AuthZObject<R>;
fallbackOnLoading?: JSX.Element;
fallbackOnError?: JSX.Element;
fallbackOnNoPermissions?: (response: {
requiredPermissionName: BrandedPermission;
}) => JSX.Element;
};
export function GuardAuthZ<R extends AuthZRelation>({
children,
relation,
object,
fallbackOnLoading,
fallbackOnError,
fallbackOnNoPermissions,
}: GuardAuthZProps<R>): JSX.Element | null {
const permission = buildPermission<R>(relation, object);
const { permissions, isLoading, error } = useAuthZ([permission]);
if (isLoading) {
return fallbackOnLoading ?? null;
}
if (error) {
return fallbackOnError ?? children;
}
if (!permissions?.[permission]?.isGranted) {
return (
fallbackOnNoPermissions?.({
requiredPermissionName: permission,
}) ?? null
);
}
return children;
}

View File

@@ -1,39 +1,18 @@
import { render, screen } from 'tests/test-utils';
import PermissionDeniedCallout from './PermissionDeniedCallout';
import {
buildPermission,
buildObjectString,
} from 'lib/authz/hooks/useAuthZ/utils';
describe('PermissionDeniedCallout', () => {
it('renders the permission name in the callout message', () => {
const deniedPermissions = [
buildPermission('read', buildObjectString('serviceaccount', '*')),
];
render(<PermissionDeniedCallout deniedPermissions={deniedPermissions} />);
render(<PermissionDeniedCallout permissionName="serviceaccount:attach" />);
expect(screen.getByText(/is not authorized/)).toBeInTheDocument();
expect(screen.getByText(/read:serviceaccount:\*/)).toBeInTheDocument();
});
it('renders multiple denied permissions', () => {
const deniedPermissions = [
buildPermission('read', buildObjectString('serviceaccount', '*')),
buildPermission('update', buildObjectString('role', 'admin')),
];
render(<PermissionDeniedCallout deniedPermissions={deniedPermissions} />);
expect(screen.getByText(/read:serviceaccount:\*/)).toBeInTheDocument();
expect(screen.getByText(/update:role:admin/)).toBeInTheDocument();
expect(screen.getByText(/serviceaccount:attach/)).toBeInTheDocument();
});
it('accepts an optional className', () => {
const deniedPermissions = [
buildPermission('read', buildObjectString('serviceaccount', '*')),
];
const { container } = render(
<PermissionDeniedCallout
deniedPermissions={deniedPermissions}
permissionName="serviceaccount:read"
className="custom-class"
/>,
);

View File

@@ -3,32 +3,18 @@ import cx from 'classnames';
import styles from './PermissionDeniedCallout.module.scss';
import { useAppContext } from 'providers/App/App';
import { Typography } from '@signozhq/ui/typography';
import { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
import { formatPermission } from 'lib/authz/hooks/useAuthZ/utils';
export interface PermissionDeniedCalloutProps {
/**
* @deprecated Use `deniedPermissions` instead. Will be removed after authz devtools PR merges.
*/
permissionName?: string;
deniedPermissions?: BrandedPermission[];
interface PermissionDeniedCalloutProps {
permissionName: string;
className?: string;
}
function PermissionDeniedCallout({
permissionName,
deniedPermissions,
className,
}: PermissionDeniedCalloutProps): JSX.Element {
const { user } = useAppContext();
// TODO(authz): Remove permissionName support after devtools PR merges
const formattedPermissions = deniedPermissions
? deniedPermissions.map(formatPermission)
: permissionName
? [permissionName]
: [];
return (
<Callout
type="error"
@@ -39,12 +25,7 @@ function PermissionDeniedCallout({
<Typography.Text className={styles.permission}>
<code className={styles.permissionCode}>user/{user.id}</code> is not
authorized to perform{' '}
{formattedPermissions.map((perm, idx) => (
<span key={perm}>
<code className={styles.permissionCode}>{perm}</code>
{idx < formattedPermissions.length - 1 && ', '}
</span>
))}
<code className={styles.permissionCode}>{permissionName}</code>
</Typography.Text>
</Callout>
);

View File

@@ -1,29 +1,17 @@
import { render, screen } from 'tests/test-utils';
import PermissionDeniedFullPage from './PermissionDeniedFullPage';
import {
buildPermission,
buildObjectString,
} from 'lib/authz/hooks/useAuthZ/utils';
describe('PermissionDeniedFullPage', () => {
it('renders the title and subtitle with the permissionName interpolated', () => {
const deniedPermissions = [
buildPermission('read', buildObjectString('serviceaccount', '*')),
];
render(<PermissionDeniedFullPage deniedPermissions={deniedPermissions} />);
render(<PermissionDeniedFullPage permissionName="serviceaccount:list" />);
expect(screen.getByText('Uh-oh! You are not authorized')).toBeInTheDocument();
expect(screen.getByText(/read:serviceaccount:\*/)).toBeInTheDocument();
expect(screen.getByText(/serviceaccount:list/)).toBeInTheDocument();
expect(screen.getByText(/is not authorized to perform/)).toBeInTheDocument();
});
it('renders with multiple denied permissions', () => {
const deniedPermissions = [
buildPermission('read', buildObjectString('role', 'admin')),
buildPermission('update', buildObjectString('role', 'admin')),
];
render(<PermissionDeniedFullPage deniedPermissions={deniedPermissions} />);
expect(screen.getByText(/read:role:admin/)).toBeInTheDocument();
expect(screen.getByText(/update:role:admin/)).toBeInTheDocument();
it('renders with a different permissionName', () => {
render(<PermissionDeniedFullPage permissionName="role:read" />);
expect(screen.getByText(/role:read/)).toBeInTheDocument();
});
});

View File

@@ -3,30 +3,16 @@ import { CircleSlash2 } from '@signozhq/icons';
import styles from './PermissionDeniedFullPage.module.scss';
import { Style } from '@signozhq/design-tokens';
import { useAppContext } from 'providers/App/App';
import { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
import { formatPermission } from 'lib/authz/hooks/useAuthZ/utils';
export interface PermissionDeniedFullPageProps {
/**
* @deprecated Use `deniedPermissions` instead. Will be removed after authz devtools PR merges.
*/
permissionName?: string;
deniedPermissions?: BrandedPermission[];
interface PermissionDeniedFullPageProps {
permissionName: string;
}
function PermissionDeniedFullPage({
permissionName,
deniedPermissions,
}: PermissionDeniedFullPageProps): JSX.Element {
const { user } = useAppContext();
// TODO(authz): Remove permissionName support after devtools PR merges
const formattedPermissions = deniedPermissions
? deniedPermissions.map(formatPermission)
: permissionName
? [permissionName]
: [];
return (
<div className={styles.container}>
<div className={styles.content}>
@@ -36,13 +22,7 @@ function PermissionDeniedFullPage({
<p className={styles.title}>Uh-oh! You are not authorized</p>
<p className={styles.subtitle}>
<code className={styles.permission}>user/{user.id}</code> is not authorized
to perform{' '}
{formattedPermissions.map((perm, idx) => (
<span key={perm}>
<code className={styles.permission}>{perm}</code>
{idx < formattedPermissions.length - 1 && ', '}
</span>
))}
to perform <code className={styles.permission}>{permissionName}</code>
</p>
</div>
</div>

View File

@@ -1,185 +0,0 @@
# AuthZ Components
Quick reference for permission-gating UI. All components use AND semantics: user needs ALL permissions in `checks` array.
## Decision Tree
```
Need to gate...
├── A button? → AuthZButton
├── Any element with tooltip on deny? → AuthZTooltip
├── A section inside a page? → withAuthZContent (preferred)
│ └── Need JSX wrapper? → AuthZGuardContent
├── An entire page/route? → withAuthZPage (preferred)
│ └── Need JSX wrapper? → AuthZGuardPage
├── Need full control over fallback? → withAuthZ / AuthZGuard
└── None of above fit?
├── Can create wrapper component? → Create it (like AuthZButton)
└── Last resort → useAuthZ hook directly
```
## Building Permissions
Use `buildPermission`, `buildObjectString` or pre-built constants. Never cast with `as BrandedPermission`.
```tsx
import { buildPermission, buildObjectString } from 'lib/authz/hooks/useAuthZ/utils';
import {
RoleCreatePermission,
buildRoleReadPermission
} from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
// Static permission (pre-built)
const checks = [RoleCreatePermission];
// Dynamic permission (builder fn)
const checks = [buildRoleReadPermission(roleId)];
// Custom permission (buildPermission + buildObjectString)
const checks = [buildPermission('read', buildObjectString('dashboard', dashboardId))];
```
## Creating Permission Helpers
When adding authz to a new resource, create a permissions file under `lib/authz/hooks/useAuthZ/permissions/`.
```tsx
// lib/authz/hooks/useAuthZ/permissions/dashboard.permissions.ts
import { buildPermission } from '../utils';
import type { BrandedPermission } from '../types';
// Collection-level — wildcard, no specific id needed
export const DashboardCreatePermission = buildPermission('create', 'dashboard:*');
export const DashboardListPermission = buildPermission('list', 'dashboard:*');
// Resource-level — require specific id
export const buildDashboardReadPermission = (id: string): BrandedPermission =>
buildPermission('read', `dashboard:${id}`);
export const buildDashboardUpdatePermission = (id: string): BrandedPermission =>
buildPermission('update', `dashboard:${id}`);
export const buildDashboardDeletePermission = (id: string): BrandedPermission =>
buildPermission('delete', `dashboard:${id}`);
```
Pattern:
- `<Resource><Action>Permission` → collection-level const (wildcard `*`)
- `build<Resource><Action>Permission(id)` → resource-level fn (specific id)
## Components
### AuthZButton
Button that disables + shows tooltip when denied.
```tsx
import { SACreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
<AuthZButton checks={[SACreatePermission]} onClick={handleCreate}>
Create
</AuthZButton>
```
### AuthZTooltip
Wraps any element. Disables child + shows denial tooltip.
```tsx
import { buildSADeletePermission } from 'lib/authz/hooks/useAuthZ/permissions/service-account.permissions';
<AuthZTooltip checks={[buildSADeletePermission(accountId)]}>
<IconButton icon={<Trash />} onClick={handleDelete} />
</AuthZTooltip>
```
### withAuthZPage (preferred for pages)
HOC for route-level gating. Wrap at export. Shows `PermissionDeniedFullPage` + `AppLoading`.
```tsx
import { RoleListPermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
function RolesPage(): JSX.Element {
return <div>...</div>;
}
export default withAuthZPage(RolesPage, {
checks: [RoleListPermission],
});
```
### withAuthZContent (preferred for sections)
HOC for inline sections. Shows `PermissionDeniedCallout` on deny.
```tsx
import { buildRoleReadPermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
function RoleEditor(): JSX.Element {
return <div>...</div>;
}
// Dynamic checks from route params
export default withAuthZContent(RoleEditor, {
checks: (_props, ctx) => [buildRoleReadPermission(ctx.params.roleId)],
});
```
### withAuthZ
HOC base. No default fallback. Use when you need custom fallback.
```tsx
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
export default withAuthZ(SecretPanel, {
checks: [buildPermission('write', 'settings:org')],
fallback: <p>No access</p>,
});
```
### AuthZGuardPage
JSX variant of `withAuthZPage`. Use when HOC not possible (conditional rendering).
```tsx
import { RoleListPermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
<AuthZGuardPage checks={[RoleListPermission]}>
<RolesPage />
</AuthZGuardPage>
```
### AuthZGuardContent
JSX variant of `withAuthZContent`. Use when HOC not possible.
```tsx
import { RoleCreatePermission } from 'lib/authz/hooks/useAuthZ/permissions/role.permissions';
<AuthZGuardContent checks={[RoleCreatePermission]}>
<RoleEditor />
</AuthZGuardContent>
```
### AuthZGuard
JSX base guard. No default fallback. Use when you need custom fallback in JSX.
```tsx
import { buildPermission } from 'lib/authz/hooks/useAuthZ/utils';
<AuthZGuard
checks={[buildPermission('write', 'settings:org')]}
fallback={<p>No access</p>}
fallbackOnLoading={<Spinner />}
>
<SecretContent />
</AuthZGuard>
```
## Fallback Components
Don't use these components directly, always prefer using via `withAuthZ` and their variants.
- PermissionDeniedCallout: inline error callout. Shows `user/{id} is not authorized to perform {permissions}`.
- PermissionDeniedFullPage: full-page centered error. Same message, bigger presentation.

View File

@@ -0,0 +1,440 @@
import { ReactElement } from 'react';
import type { RouteComponentProps } from 'react-router-dom';
import type {
AuthtypesGettableTransactionDTO,
AuthtypesTransactionDTO,
} from 'api/generated/services/sigNoz.schemas';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, waitFor } from 'tests/test-utils';
import {
AUTHZ_CHECK_URL,
authzMockResponse,
} from 'lib/authz/utils/authz-test-utils';
import { createGuardedRoute } from './createGuardedRoute';
describe('createGuardedRoute', () => {
const TestComponent = ({ testProp }: { testProp: string }): ReactElement => (
<div>Test Component: {testProp}</div>
);
it('should render component when permission is granted', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(TestComponent, 'read', 'role:*');
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should substitute route parameters in object string', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'role:{id}',
);
const mockMatch = {
params: { id: '123' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/123',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should handle multiple route parameters', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = (await req.json()) as AuthtypesTransactionDTO[];
const txn = payload[0];
const responseData: AuthtypesGettableTransactionDTO[] = [
{
relation: txn.relation,
object: {
resource: {
kind: txn.object.resource.kind,
type: txn.object.resource.type,
},
selector: '123:456',
},
authorized: true,
},
];
return res(
ctx.status(200),
ctx.json({ data: responseData, status: 'success' }),
);
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'update',
'role:{id}:{version}',
);
const mockMatch = {
params: { id: '123', version: '456' },
isExact: true,
path: '/dashboard/:id/:version',
url: '/dashboard/123/456',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should keep placeholder when route parameter is missing', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'role:{id}',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should render loading fallback when loading', () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (_req, res, ctx) => {
return res(
ctx.delay('infinite'),
ctx.status(200),
ctx.json({ data: [], status: 'success' }),
);
}),
);
const GuardedComponent = createGuardedRoute(TestComponent, 'read', 'role:*');
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
expect(screen.getByText('SigNoz')).toBeInTheDocument();
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
});
it('should render the component when API error occurs (fail open)', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Internal Server Error' }));
}),
);
const GuardedComponent = createGuardedRoute(TestComponent, 'read', 'role:*');
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
it('should render no permissions fallback when permission is denied', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [false])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'update',
'role:{id}',
);
const mockMatch = {
params: { id: '123' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/123',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
const heading = document.querySelector('h3');
expect(heading).toBeInTheDocument();
expect(heading?.textContent).toMatch(/not authorized/i);
});
expect(screen.getByText(/update:role:123/)).toBeInTheDocument();
expect(
screen.queryByText('Test Component: test-value'),
).not.toBeInTheDocument();
});
it('should pass all props to wrapped component', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const ComponentWithMultipleProps = ({
prop1,
prop2,
prop3,
}: {
prop1: string;
prop2: number;
prop3: boolean;
}): ReactElement => (
<div>
{prop1} - {prop2} - {prop3.toString()}
</div>
);
const GuardedComponent = createGuardedRoute(
ComponentWithMultipleProps,
'read',
'role:*',
);
const mockMatch = {
params: {},
isExact: true,
path: '/dashboard',
url: '/dashboard',
};
const props = {
prop1: 'value1',
prop2: 42,
prop3: true,
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('value1 - 42 - true')).toBeInTheDocument();
});
});
it('should memoize resolved object based on route params', async () => {
let requestCount = 0;
const requestedObjects: string[] = [];
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
requestCount++;
const payload = (await req.json()) as AuthtypesTransactionDTO[];
const obj = payload[0]?.object;
const kind = obj?.resource?.kind;
const selector = obj?.selector ?? '*';
const objectStr = `${kind}:${selector}`;
requestedObjects.push(objectStr ?? '');
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'read',
'role:{id}',
);
const mockMatch1 = {
params: { id: '123' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/123',
};
const props1 = {
testProp: 'test-value-1',
match: mockMatch1,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
const { unmount } = render(<GuardedComponent {...props1} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value-1')).toBeInTheDocument();
});
expect(requestCount).toBe(1);
expect(requestedObjects).toContain('role:123');
unmount();
const mockMatch2 = {
params: { id: '456' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/456',
};
const props2 = {
testProp: 'test-value-2',
match: mockMatch2,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props2} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value-2')).toBeInTheDocument();
});
expect(requestCount).toBe(2);
expect(requestedObjects).toContain('role:456');
});
it('should handle different relation types', async () => {
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true])));
}),
);
const GuardedComponent = createGuardedRoute(
TestComponent,
'delete',
'role:{id}',
);
const mockMatch = {
params: { id: '789' },
isExact: true,
path: '/dashboard/:id',
url: '/dashboard/789',
};
const props = {
testProp: 'test-value',
match: mockMatch,
location: {} as unknown as RouteComponentProps['location'],
history: {} as unknown as RouteComponentProps['history'],
};
render(<GuardedComponent {...props} />);
await waitFor(() => {
expect(screen.getByText('Test Component: test-value')).toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,41 @@
.guard-authz-error-no-authz {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 24px;
.guard-authz-error-no-authz-content {
display: flex;
flex-direction: column;
justify-content: flex-start;
gap: 8px;
max-width: 500px;
}
img {
width: 32px;
height: 32px;
}
h3 {
font-size: 18px;
color: var(--l1-foreground);
line-height: 18px;
}
p {
font-size: 14px;
color: var(--l3-foreground);
line-height: 18px;
span {
background-color: var(--l3-background);
white-space: nowrap;
padding: 0 2px;
}
}
}

View File

@@ -0,0 +1,67 @@
import { ComponentType, ReactElement, useMemo } from 'react';
import { RouteComponentProps } from 'react-router-dom';
import {
AuthZObject,
AuthZRelation,
BrandedPermission,
} from 'lib/authz/hooks/useAuthZ/types';
import { formatPermission } from 'lib/authz/hooks/useAuthZ/utils';
import { useAppContext } from 'providers/App/App';
import noDataUrl from 'assets/Icons/no-data.svg';
import AppLoading from '../../../../components/AppLoading/AppLoading';
import { GuardAuthZ } from '../GuardAuthZ/GuardAuthZ';
import './createGuardedRoute.styles.scss';
function OnNoPermissionsFallback(response: {
requiredPermissionName: BrandedPermission;
}): ReactElement {
const { user } = useAppContext();
return (
<div className="guard-authz-error-no-authz">
<div className="guard-authz-error-no-authz-content">
<img src={noDataUrl} alt="No permission" />
<h3>Uh-oh! You are not authorized</h3>
<p>
<code>user/{user.id}</code> is not authorized to perform{' '}
<code>{formatPermission(response.requiredPermissionName)}</code>
</p>
</div>
</div>
);
}
// eslint-disable-next-line @typescript-eslint/ban-types
export function createGuardedRoute<P extends object, R extends AuthZRelation>(
Component: ComponentType<P>,
relation: R,
object: AuthZObject<R>,
): ComponentType<P & RouteComponentProps<Record<string, string>>> {
return function GuardedRouteComponent(
props: P & RouteComponentProps<Record<string, string>>,
): ReactElement {
const resolvedObject = useMemo(() => {
const paramPattern = /\{([^}]+)\}/g;
return object.replace(paramPattern, (match, paramName) => {
const paramValue = props.match?.params?.[paramName];
return paramValue !== undefined ? paramValue : match;
}) as AuthZObject<R>;
}, [props.match?.params]);
return (
<GuardAuthZ
relation={relation}
object={resolvedObject}
fallbackOnLoading={<AppLoading />}
fallbackOnNoPermissions={(response): ReactElement => (
<OnNoPermissionsFallback {...response} />
)}
>
<Component {...props} />
</GuardAuthZ>
);
};
}

View File

@@ -1,578 +0,0 @@
import React from 'react';
import { render, screen, waitFor, act } from 'tests/test-utils';
import { useQueryClient } from 'react-query';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import {
AUTHZ_CHECK_URL,
authzMockResponse,
setupAuthzAllow,
setupAuthzDeny,
setupAuthzGrantByPrefix,
} from 'lib/authz/utils/authz-test-utils';
import type { AuthZObject } from 'lib/authz/hooks/useAuthZ/types';
import {
buildObjectString,
buildPermission,
} from 'lib/authz/hooks/useAuthZ/utils';
import { withAuthZ, RouterContext } from './withAuthZ';
import { withAuthZContent } from './withAuthZContent';
import { withAuthZPage } from './withAuthZPage';
const mockUseParams = jest.fn();
const mockUseLocation = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useParams: (): Record<string, string> => mockUseParams(),
useLocation: (): { pathname: string; search: string } => mockUseLocation(),
}));
const readPerm = buildPermission('read', 'role:*' as AuthZObject<'read'>);
function Base(): JSX.Element {
return <div>Base component</div>;
}
beforeEach(() => {
mockUseParams.mockReturnValue({});
mockUseLocation.mockReturnValue({ pathname: '/', search: '' });
});
describe('withAuthZ', () => {
it('renders the wrapped component when allowed', async () => {
server.use(setupAuthzAllow(readPerm));
const Guarded = withAuthZ(Base, { checks: [readPerm] });
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('Base component')).toBeInTheDocument();
});
});
it('renders nothing when denied without a fallback', async () => {
server.use(setupAuthzDeny(readPerm));
const Guarded = withAuthZ(Base, { checks: [readPerm] });
render(<Guarded />);
await waitFor(() => {
expect(screen.queryByText('Base component')).not.toBeInTheDocument();
});
});
it('renders the provided fallback when denied', async () => {
server.use(setupAuthzDeny(readPerm));
const Guarded = withAuthZ(Base, {
checks: [readPerm],
fallback: <div>No access</div>,
});
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('No access')).toBeInTheDocument();
});
});
it('resolves checks from props via the selector form', async () => {
type Props = { roleId: string };
const RoleView = ({ roleId }: Props): JSX.Element => <div>role {roleId}</div>;
const deniedPerm = buildPermission(
'read',
buildObjectString<'read'>('role', 'r-1'),
);
server.use(setupAuthzDeny(deniedPerm));
const Guarded = withAuthZ<Props>(RoleView, {
checks: ({ roleId }) => [
buildPermission('read', buildObjectString<'read'>('role', roleId)),
],
fallback: <div>denied selector</div>,
});
render(<Guarded roleId="r-1" />);
await waitFor(() => {
expect(screen.getByText('denied selector')).toBeInTheDocument();
});
expect(screen.queryByText('role r-1')).not.toBeInTheDocument();
});
it('sets a descriptive displayName', () => {
const Guarded = withAuthZ(Base, { checks: [readPerm] });
expect(Guarded.displayName).toBe('withAuthZ(Base)');
});
});
describe('withAuthZPage', () => {
it('renders the full-page denied screen when denied', async () => {
server.use(setupAuthzDeny(readPerm));
const Guarded = withAuthZPage(Base, { checks: [readPerm] });
render(<Guarded />);
await waitFor(() => {
expect(
screen.getByText('Uh-oh! You are not authorized'),
).toBeInTheDocument();
});
});
});
describe('withAuthZContent', () => {
it('renders the denied callout when denied', async () => {
server.use(setupAuthzDeny(readPerm));
const Guarded = withAuthZContent(Base, { checks: [readPerm] });
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('read:role:*')).toBeInTheDocument();
});
expect(screen.queryByText('Base component')).not.toBeInTheDocument();
});
});
describe('withAuthZ router context', () => {
it('extracts checks from route params via router.params', async () => {
mockUseParams.mockReturnValue({ roleId: 'r-123' });
const rolePerm = buildPermission(
'read',
buildObjectString<'read'>('role', 'r-123'),
);
server.use(setupAuthzAllow(rolePerm));
const RoleView = (): JSX.Element => <div>role view</div>;
const Guarded = withAuthZ(RoleView, {
checks: (_props, router: RouterContext) => [
buildPermission(
'read',
buildObjectString<'read'>('role', router.params.roleId ?? ''),
),
],
});
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('role view')).toBeInTheDocument();
});
});
it('extracts checks from query params via router.searchParams', async () => {
mockUseLocation.mockReturnValue({
pathname: '/roles',
search: '?roleId=r-456',
});
const rolePerm = buildPermission(
'read',
buildObjectString<'read'>('role', 'r-456'),
);
server.use(setupAuthzAllow(rolePerm));
const RoleListView = (): JSX.Element => <div>role list view</div>;
const Guarded = withAuthZ(RoleListView, {
checks: (_props, router: RouterContext) => [
buildPermission(
'read',
buildObjectString<'read'>('role', router.searchParams.get('roleId') ?? ''),
),
],
});
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('role list view')).toBeInTheDocument();
});
});
it('extracts checks from pathname via router.matchPath', async () => {
mockUseLocation.mockReturnValue({
pathname: '/settings/roles/r-789/edit',
search: '',
});
const rolePerm = buildPermission(
'update',
buildObjectString<'update'>('role', 'r-789'),
);
server.use(setupAuthzAllow(rolePerm));
const EditRoleView = (): JSX.Element => <div>edit role</div>;
const Guarded = withAuthZ(EditRoleView, {
checks: (_props, router: RouterContext) => {
const match = router.matchPath<{ roleId: string }>(
'/settings/roles/:roleId/edit',
);
return match
? [
buildPermission(
'update',
buildObjectString<'update'>('role', match.roleId),
),
]
: [];
},
});
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('edit role')).toBeInTheDocument();
});
});
it('denies when router-derived permission is not allowed', async () => {
mockUseParams.mockReturnValue({ roleId: 'r-denied' });
const deniedPerm = buildPermission(
'read',
buildObjectString<'read'>('role', 'r-denied'),
);
server.use(setupAuthzDeny(deniedPerm));
const RoleView = (): JSX.Element => <div>role view</div>;
const Guarded = withAuthZ(RoleView, {
checks: (_props, router: RouterContext) => [
buildPermission(
'read',
buildObjectString<'read'>('role', router.params.roleId ?? ''),
),
],
fallback: <div>access denied</div>,
});
render(<Guarded />);
await waitFor(() => {
expect(screen.getByText('access denied')).toBeInTheDocument();
});
expect(screen.queryByText('role view')).not.toBeInTheDocument();
});
});
describe('withAuthZ router context stability', () => {
let renderCount = 0;
function RenderCounter(): JSX.Element {
renderCount += 1;
return <div data-testid="render-count">{renderCount}</div>;
}
beforeEach(() => {
renderCount = 0;
server.use(setupAuthzGrantByPrefix('read||__||role'));
});
it('does not re-render when useParams returns new object with same values', async () => {
mockUseParams.mockReturnValue({ roleId: 'r-1' });
mockUseLocation.mockReturnValue({ pathname: '/', search: '' });
const Guarded = withAuthZ(RenderCounter, {
checks: (_props, router: RouterContext) => [
buildPermission(
'read',
buildObjectString<'read'>('role', router.params.roleId ?? '*'),
),
],
});
const { rerender } = render(<Guarded />);
await waitFor(() => {
expect(screen.getByTestId('render-count')).toBeInTheDocument();
});
const initialCount = renderCount;
// Return NEW object with SAME values — should not cause re-render
mockUseParams.mockReturnValue({ roleId: 'r-1' });
rerender(<Guarded />);
// Allow any pending effects to flush
await waitFor(() => {
expect(renderCount).toBe(initialCount);
});
});
it('does not re-render when useLocation returns new object with same pathname', async () => {
mockUseParams.mockReturnValue({});
mockUseLocation.mockReturnValue({ pathname: '/roles', search: '' });
const Guarded = withAuthZ(RenderCounter, {
checks: (_props, router: RouterContext) => {
const match = router.matchPath<{ id: string }>('/roles/:id');
return match
? [buildPermission('read', buildObjectString<'read'>('role', match.id))]
: [readPerm];
},
});
const { rerender } = render(<Guarded />);
await waitFor(() => {
expect(screen.getByTestId('render-count')).toBeInTheDocument();
});
const initialCount = renderCount;
// Return NEW object with SAME pathname — should not cause re-render
mockUseLocation.mockReturnValue({ pathname: '/roles', search: '' });
rerender(<Guarded />);
await waitFor(() => {
expect(renderCount).toBe(initialCount);
});
});
it('does not re-render when useLocation returns new object with same search params', async () => {
mockUseParams.mockReturnValue({});
mockUseLocation.mockReturnValue({ pathname: '/', search: '?tab=keys' });
const Guarded = withAuthZ(RenderCounter, {
checks: (_props, router: RouterContext) => {
// Access searchParams to ensure it's part of the dependency chain
void router.searchParams.get('tab');
return [readPerm];
},
});
const { rerender } = render(<Guarded />);
await waitFor(() => {
expect(screen.getByTestId('render-count')).toBeInTheDocument();
});
const initialCount = renderCount;
// Return NEW object with SAME search — should not cause re-render
mockUseLocation.mockReturnValue({ pathname: '/', search: '?tab=keys' });
rerender(<Guarded />);
await waitFor(() => {
expect(renderCount).toBe(initialCount);
});
});
it('re-renders when params values actually change', async () => {
mockUseParams.mockReturnValue({ roleId: 'r-1' });
mockUseLocation.mockReturnValue({ pathname: '/', search: '' });
const Guarded = withAuthZ(RenderCounter, {
checks: (_props, router: RouterContext) => [
buildPermission(
'read',
buildObjectString<'read'>('role', router.params.roleId ?? '*'),
),
],
});
const { unmount } = render(<Guarded />);
await waitFor(() => {
expect(screen.getByTestId('render-count')).toBeInTheDocument();
});
const initialCount = renderCount;
unmount();
// Return DIFFERENT values — re-mount with new mock values
mockUseParams.mockReturnValue({ roleId: 'r-2' });
render(<Guarded />);
await waitFor(() => {
expect(renderCount).toBeGreaterThan(initialCount);
});
});
it('re-renders when pathname actually changes', async () => {
mockUseParams.mockReturnValue({});
mockUseLocation.mockReturnValue({ pathname: '/roles', search: '' });
const Guarded = withAuthZ(RenderCounter, {
checks: [readPerm],
});
const { unmount } = render(<Guarded />);
await waitFor(() => {
expect(screen.getByTestId('render-count')).toBeInTheDocument();
});
const initialCount = renderCount;
unmount();
// DIFFERENT pathname — re-mount with new mock values
mockUseLocation.mockReturnValue({ pathname: '/users', search: '' });
render(<Guarded />);
await waitFor(() => {
expect(renderCount).toBeGreaterThan(initialCount);
});
});
it('re-renders when search params actually change', async () => {
mockUseParams.mockReturnValue({});
mockUseLocation.mockReturnValue({ pathname: '/', search: '?tab=keys' });
const Guarded = withAuthZ(RenderCounter, {
checks: [readPerm],
});
const { unmount } = render(<Guarded />);
await waitFor(() => {
expect(screen.getByTestId('render-count')).toBeInTheDocument();
});
const initialCount = renderCount;
unmount();
// DIFFERENT search — re-mount with new mock values
mockUseLocation.mockReturnValue({ pathname: '/', search: '?tab=details' });
render(<Guarded />);
await waitFor(() => {
expect(renderCount).toBeGreaterThan(initialCount);
});
});
});
describe('withAuthZContent cache invalidation', () => {
const testPerm = buildPermission(
'read',
'role:test-invalidation' as AuthZObject<'read'>,
);
// Callout displays permission as "relation:object" format
const displayedPerm = 'read:role:test-invalidation';
function ContentComponent(): JSX.Element {
return <div data-testid="protected-content">Protected Content</div>;
}
function InvalidationTrigger({
permission,
onReady,
}: {
permission: string;
onReady: (invalidate: () => Promise<void>) => void;
}): null {
const queryClient = useQueryClient();
React.useEffect(() => {
onReady(async () => {
// Reset query to initial state and trigger refetch (matches devtools behavior)
await queryClient.resetQueries(['authz', permission]);
});
}, [queryClient, permission, onReady]);
return null;
}
it('re-renders from allowed to denied when cache is invalidated', async () => {
let shouldGrant = true;
let invalidateFn: (() => Promise<void>) | null = null;
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(
ctx.status(200),
ctx.json(authzMockResponse(payload, [shouldGrant])),
);
}),
);
const Guarded = withAuthZContent(ContentComponent, { checks: [testPerm] });
render(
<>
<Guarded />
<InvalidationTrigger
permission={testPerm}
onReady={(fn): void => {
invalidateFn = fn;
}}
/>
</>,
);
// Initially allowed - should show content
await waitFor(() => {
expect(screen.getByTestId('protected-content')).toBeInTheDocument();
});
// Change server response to deny
shouldGrant = false;
// Invalidate cache
await act(async () => {
await invalidateFn?.();
});
// Should now show denied callout
await waitFor(() => {
expect(screen.queryByTestId('protected-content')).not.toBeInTheDocument();
});
// Callout should show the denied permission
expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByText(displayedPerm)).toBeInTheDocument();
});
it('re-renders from denied to allowed when cache is invalidated', async () => {
let shouldGrant = false;
let invalidateFn: (() => Promise<void>) | null = null;
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
return res(
ctx.status(200),
ctx.json(authzMockResponse(payload, [shouldGrant])),
);
}),
);
const Guarded = withAuthZContent(ContentComponent, { checks: [testPerm] });
render(
<>
<Guarded />
<InvalidationTrigger
permission={testPerm}
onReady={(fn): void => {
invalidateFn = fn;
}}
/>
</>,
);
// Initially denied - should show callout
await waitFor(() => {
expect(screen.getByRole('alert')).toBeInTheDocument();
});
expect(screen.queryByTestId('protected-content')).not.toBeInTheDocument();
// Change server response to allow
shouldGrant = true;
// Invalidate cache
await act(async () => {
await invalidateFn?.();
});
// Should now show protected content
await waitFor(() => {
expect(screen.getByTestId('protected-content')).toBeInTheDocument();
});
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
});

View File

@@ -1,13 +0,0 @@
import { ComponentType } from 'react';
import { AuthZGuard } from 'lib/authz/components/AuthZGuard/AuthZGuard';
import { createAuthZHOC, WithAuthZOptions } from './withAuthZ.utils';
export type { RouterContext, WithAuthZOptions } from './withAuthZ.utils';
export function withAuthZ<P extends object>(
Component: ComponentType<P>,
opts: WithAuthZOptions<P>,
): ComponentType<P> {
return createAuthZHOC(AuthZGuard, 'withAuthZ', Component, opts);
}

View File

@@ -1,108 +0,0 @@
import { ComponentType, ReactElement, createElement, useMemo } from 'react';
import {
matchPath as reactRouterMatchPath,
useLocation,
useParams,
} from 'react-router-dom';
import type { AuthZGuardProps } from 'lib/authz/components/AuthZGuard/AuthZGuard';
import type { BrandedPermission } from 'lib/authz/hooks/useAuthZ/types';
export type RouterContext = {
/**
* Route params from useParams (e.g. `/roles/:roleId` → `{ roleId: "r-1" }`)
*/
params: Record<string, string | undefined>;
pathname: string;
/**
* Query params as URLSearchParams (use `.get('key')` to read)
*/
searchParams: URLSearchParams;
/**
* Extract params from pathname using a route pattern.
* Returns null if pattern doesn't match.
* @example router.matchPath<{ id: string }>('/edit/:id')?.id
*/
matchPath: <Params extends Record<string, string>>(
pattern: string,
) => Params | null;
};
export type WithAuthZOptions<P> = {
/**
* Static checks, or a selector deriving them from props and router context.
* Use router context to extract dynamic values from route params, pathname, or query params.
* @example
* // From route params
* checks: (props, router) => [buildPermission('read', `role:${router.params.roleId}`)]
* // From query params
* checks: (props, router) => [buildPermission('read', `dashboard:${router.searchParams.get('id')}`)]
* // From pathname matching
* checks: (props, router) => {
* const match = router.matchPath<{ id: string }>('/edit/:id');
* return match ? [buildPermission('update', `role:${match.id}`)] : [];
* }
*/
checks:
| BrandedPermission[]
| ((props: P, router: RouterContext) => BrandedPermission[]);
fallback?: AuthZGuardProps['fallback'];
fallbackOnLoading?: AuthZGuardProps['fallbackOnLoading'];
failOpenOnError?: AuthZGuardProps['onFailRenderContent'];
};
function useStableParams(): Record<string, string | undefined> {
const params = useParams();
const paramsJson = JSON.stringify(params);
return useMemo(() => JSON.parse(paramsJson), [paramsJson]);
}
function useRouterContext(): RouterContext {
const params = useStableParams();
const { pathname, search } = useLocation();
const searchParams = useMemo(() => new URLSearchParams(search), [search]);
return useMemo(
(): RouterContext => ({
params,
pathname,
searchParams,
matchPath: <Params extends Record<string, string>>(
pattern: string,
): Params | null => {
const match = reactRouterMatchPath<Params>(pathname, {
path: pattern,
exact: false,
});
return match?.params ?? null;
},
}),
[params, pathname, searchParams],
);
}
export function createAuthZHOC<P extends object>(
Guard: ComponentType<AuthZGuardProps>,
hocName: string,
Component: ComponentType<P>,
opts: WithAuthZOptions<P>,
): ComponentType<P> {
const { checks, ...guardProps } = opts;
function Wrapped(props: P): ReactElement | null {
const router = useRouterContext();
const resolvedChecks =
typeof checks === 'function' ? checks(props, router) : checks;
return (
<Guard checks={resolvedChecks} {...guardProps}>
{createElement(Component, props)}
</Guard>
);
}
Wrapped.displayName = `${hocName}(${
Component.displayName || Component.name || 'Component'
})`;
return Wrapped;
}

View File

@@ -1,11 +0,0 @@
import { ComponentType } from 'react';
import { AuthZGuardContent } from 'lib/authz/components/AuthZGuard/AuthZGuardContent';
import { createAuthZHOC, WithAuthZOptions } from './withAuthZ.utils';
export function withAuthZContent<P extends object>(
Component: ComponentType<P>,
opts: WithAuthZOptions<P>,
): ComponentType<P> {
return createAuthZHOC(AuthZGuardContent, 'withAuthZContent', Component, opts);
}

View File

@@ -1,11 +0,0 @@
import { ComponentType } from 'react';
import { AuthZGuardPage } from 'lib/authz/components/AuthZGuard/AuthZGuardPage';
import { createAuthZHOC, WithAuthZOptions } from './withAuthZ.utils';
export function withAuthZPage<P extends object>(
Component: ComponentType<P>,
opts: WithAuthZOptions<P>,
): ComponentType<P> {
return createAuthZHOC(AuthZGuardPage, 'withAuthZPage', Component, opts);
}

View File

@@ -1,125 +0,0 @@
# AuthZ Test Utilities
Helpers for testing permission-gated components.
## File Naming
AuthZ tests live in `*.authz.test.tsx` files alongside other test files:
```
ComponentName/
├── ComponentName.tsx
├── __tests__/
│ ├── ComponentName.test.tsx # functional tests
│ └── ComponentName.authz.test.tsx # permission tests
```
## Test Structure
```tsx
import { server } from 'mocks-server/server';
import { setupAuthzAdmin, setupAuthzDenyAll } from 'lib/authz/utils/authz-test-utils';
import { render, screen, waitFor } from 'tests/test-utils';
describe('ComponentName - AuthZ', () => {
afterEach(() => {
jest.restoreAllMocks();
server.resetHandlers(); // reset MSW handlers after each test
});
describe('permission denied', () => {
it('shows permission denied when read denied', async () => {
server.use(setupAuthzDenyAll());
render(<ComponentName />);
await expect(
screen.findByText(/not authorized/i),
).resolves.toBeInTheDocument();
});
});
describe('permission granted', () => {
it('renders content when permitted', async () => {
server.use(setupAuthzAdmin());
render(<ComponentName />);
await waitFor(() => {
expect(screen.getByTestId('protected-content')).toBeInTheDocument();
});
});
});
});
```
Key points:
- Use `server.use()` at start of each test (not `beforeEach`) for explicit setup
- Call `server.resetHandlers()` in `afterEach` to avoid test pollution
- Use `waitFor` or `findBy*` queries since authz checks are async
- Group tests by permission scenario: denied, granted, partial, loading
## MSW Handlers
Mock `/api/v1/authz/check` endpoint responses.
```tsx
import { server } from 'mocks-server/server';
import {
setupAuthzAdmin,
setupAuthzDenyAll,
setupAuthzDeny,
setupAuthzAllow,
setupAuthzGrantByPrefix,
} from 'lib/authz/utils/authz-test-utils';
// Grant all permissions
server.use(setupAuthzAdmin());
// Deny all permissions
server.use(setupAuthzDenyAll());
// Grant all except specific permissions
server.use(setupAuthzDeny(RoleCreatePermission, RoleDeletePermission));
// Deny all except specific permissions
server.use(setupAuthzAllow(RoleListPermission));
// Grant by relation prefix (e.g., grant read/delete, deny update)
server.use(setupAuthzGrantByPrefix('read', 'delete'));
```
## Custom Mock Response
For fine-grained control over responses.
```tsx
import { rest } from 'msw';
import { AUTHZ_CHECK_URL, authzMockResponse } from 'lib/authz/utils/authz-test-utils';
server.use(
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = await req.json();
// [true, false] = first permission granted, second denied
return res(ctx.status(200), ctx.json(authzMockResponse(payload, [true, false])));
}),
);
```
## Testing Loading State
Use `ctx.delay('infinite')` to hold response indefinitely:
```tsx
it('shows skeleton while checking permissions', () => {
server.use(
rest.post(AUTHZ_CHECK_URL, (_req, res, ctx) =>
res(ctx.delay('infinite')),
),
);
render(<ComponentName />);
expect(document.querySelector('.ant-skeleton')).toBeInTheDocument();
});
```

View File

@@ -104,25 +104,6 @@ export function setupAuthzAllow(
});
}
/** Grants permissions that start with any of the given prefixes. */
export function setupAuthzGrantByPrefix(...prefixes: string[]): RestHandler {
return rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
const payload = (await req.json()) as AuthtypesTransactionDTO[];
return res(
ctx.status(200),
ctx.json(
authzMockResponse(
payload,
payload.map((txn) => {
const perm = gettableTransactionToPermission(txn);
return prefixes.some((prefix) => perm.startsWith(prefix));
}),
),
),
);
});
}
export function buildLicense(
overrides?: Partial<LicenseResModel>,
): LicenseResModel {

View File

@@ -32,34 +32,6 @@
cursor: help;
}
.publicLink {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 0;
border: none;
background: transparent;
color: inherit;
cursor: pointer;
}
.lockButton {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
padding: 0;
border: none;
background: transparent;
color: inherit;
cursor: pointer;
}
.lockButton:disabled {
cursor: default;
}
.divider {
flex-shrink: 0;
width: 1px;

View File

@@ -1,20 +1,12 @@
import { KeyboardEvent } from 'react';
import {
Check,
Globe,
LockKeyhole,
LockKeyholeOpen,
SolidInfoCircle,
X,
} from '@signozhq/icons';
import TagBadge from 'components/TagBadge/TagBadge';
import { Check, Globe, LockKeyhole, SolidInfoCircle, X } from '@signozhq/icons';
import { Badge } from '@signozhq/ui/badge';
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 cx from 'classnames';
import { isEmpty } from 'lodash-es';
import { openInNewTab } from 'utils/navigation';
import styles from './DashboardInfo.module.scss';
import { useVisibleTagCount } from './useVisibleTagCount';
@@ -26,13 +18,7 @@ interface DashboardInfoProps {
tags: string[];
description: string;
isPublicDashboard: boolean;
/** Absolute URL of the public dashboard page; opened when the globe is clicked. */
publicUrl: string;
isDashboardLocked: boolean;
/** Whether to render the lock toggle at all (hidden for never-locked dashboards). */
showLockToggle: boolean;
/** When provided, the lock icon toggles lock/unlock (author/admin only). */
onToggleLock?: () => void;
isEditing: boolean;
draft: string;
onDraftChange: (value: string) => void;
@@ -47,10 +33,7 @@ function DashboardInfo({
tags,
description,
isPublicDashboard,
publicUrl,
isDashboardLocked,
showLockToggle,
onToggleLock,
isEditing,
draft,
onDraftChange,
@@ -68,17 +51,6 @@ function DashboardInfo({
const visibleTags = needsOverflow ? tags.slice(0, visibleCount) : tags;
const remainingTags = needsOverflow ? tags.slice(visibleCount) : [];
let lockTooltip: string;
if (onToggleLock) {
lockTooltip = isDashboardLocked
? 'Locked — click to unlock'
: 'Unlocked — click to lock';
} else {
lockTooltip = isDashboardLocked
? 'This dashboard is locked'
: 'This dashboard is unlocked';
}
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>): void => {
if (event.key === 'Enter') {
event.preventDefault();
@@ -129,7 +101,7 @@ function DashboardInfo({
</Button>
</div>
) : (
<TooltipSimple title={title} disableHoverableContent>
<TooltipSimple title={title}>
<Typography.Text
className={cx(styles.dashboardTitle, {
[styles.dashboardTitleHover]: canEdit,
@@ -143,7 +115,7 @@ function DashboardInfo({
)}
{hasDescription && (
<TooltipSimple title={description} disableHoverableContent>
<TooltipSimple title={description}>
<SolidInfoCircle
className={styles.descriptionIcon}
size={14}
@@ -153,44 +125,14 @@ function DashboardInfo({
)}
{isPublicDashboard && (
<TooltipSimple
title="This dashboard is publicly accessible. Click to open the public page."
disableHoverableContent
>
<Button
type="button"
variant="ghost"
color="secondary"
size="icon"
className={styles.publicLink}
aria-label="Open public dashboard"
testId="dashboard-public-link"
onClick={(): void => openInNewTab(publicUrl)}
>
<Globe size={14} />
</Button>
<TooltipSimple title="This dashboard is publicly accessible">
<Globe size={14} />
</TooltipSimple>
)}
{showLockToggle && (
<TooltipSimple title={lockTooltip} disableHoverableContent>
<Button
type="button"
variant="ghost"
color="secondary"
size="icon"
className={styles.lockButton}
aria-label={isDashboardLocked ? 'Unlock dashboard' : 'Lock dashboard'}
testId="dashboard-lock"
disabled={!onToggleLock}
onClick={onToggleLock}
>
{isDashboardLocked ? (
<LockKeyhole size={14} />
) : (
<LockKeyholeOpen size={14} />
)}
</Button>
{isDashboardLocked && (
<TooltipSimple title="This dashboard is locked">
<LockKeyhole size={14} />
</TooltipSimple>
)}
@@ -203,13 +145,19 @@ function DashboardInfo({
data-testid="dashboard-tags"
>
{visibleTags.map((tag) => (
<TagBadge key={tag}>{tag}</TagBadge>
<Badge key={tag} color="warning" variant="outline">
{tag}
</Badge>
))}
{remainingTags.length > 0 && (
<TooltipSimple title={remainingTags.join(', ')}>
<span data-testid="dashboard-tags-overflow">
<TagBadge>+{remainingTags.length}</TagBadge>
</span>
<Badge
color="warning"
variant="outline"
data-testid="dashboard-tags-overflow"
>
+{remainingTags.length}
</Badge>
</TooltipSimple>
)}
</div>

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useMemo } from 'react';
import { FullScreenHandle } from 'react-full-screen';
import { toast } from '@signozhq/ui/sonner';
import logEvent from 'api/common/logEvent';
@@ -15,12 +15,9 @@ import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { useAppContext } from 'providers/App/App';
import { useErrorModal } from 'providers/ErrorModalProvider';
import APIError from 'types/api/error';
import { USER_ROLES } from 'types/roles';
import { getAbsoluteUrl } from 'utils/basePath';
import { useCreatePanel } from '../hooks/useCreatePanel';
import { useOptimisticPatch } from '../hooks/useOptimisticPatch';
import { usePublicDashboardMeta } from '../DashboardSettings/PublicDashboard/usePublicDashboardMeta';
import PanelTypeSelectionModal from '../PanelsAndSectionsLayout/Panel/PanelTypeSelectionModal/PanelTypeSelectionModal';
import DashboardActions from './DashboardActions/DashboardActions';
import DashboardInfo from './DashboardInfo/DashboardInfo';
@@ -39,15 +36,7 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
const { dashboard, handle, refetch } = props;
const id = dashboard.id;
// Session-local lock state: the toggle appears once locked and persists for the page.
const [isDashboardLocked, setIsDashboardLocked] = useState(!!dashboard.locked);
const [showLockToggle, setShowLockToggle] = useState(!!dashboard.locked);
useEffect(() => {
setIsDashboardLocked(!!dashboard.locked);
setShowLockToggle(!!dashboard.locked);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dashboard.id]);
const isDashboardLocked = !!dashboard.locked;
const title = dashboard.spec.display.name;
const description = dashboard.spec.display.description ?? '';
@@ -69,36 +58,20 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
const isAuthor =
!!user?.email && !!dashboard.createdBy && dashboard.createdBy === user.email;
// Author/admin can lock-unlock (mirrors the Actions menu gate); integration-owned
// dashboards are never toggleable.
const canToggleLock =
(isAuthor || user.role === USER_ROLES.ADMIN) &&
dashboard.createdBy !== 'integration';
// Public-sharing meta (deduped react-query read); drives the header globe.
const { isPublic, publicMeta } = usePublicDashboardMeta(id);
const publicUrl = getAbsoluteUrl(publicMeta?.publicPath ?? '');
const handleLockDashboardToggle = useCallback(async (): Promise<void> => {
if (!id) {
return;
}
const next = !isDashboardLocked;
setIsDashboardLocked(next);
if (next) {
setShowLockToggle(true);
}
try {
if (next) {
await lockDashboardV2({ id });
toast.success('Dashboard locked');
} else {
if (isDashboardLocked) {
await unlockDashboardV2({ id });
toast.success('Dashboard unlocked');
} else {
await lockDashboardV2({ id });
toast.success('Dashboard locked');
}
refetch();
} catch (error) {
setIsDashboardLocked(!next);
showErrorModal(error as APIError);
}
}, [id, isDashboardLocked, refetch, showErrorModal]);
@@ -146,11 +119,8 @@ function DashboardPageToolbar(props: DashboardPageToolbarProps): JSX.Element {
image={image}
tags={tags}
description={description}
isPublicDashboard={isPublic}
publicUrl={publicUrl}
isPublicDashboard={false}
isDashboardLocked={isDashboardLocked}
showLockToggle={showLockToggle}
onToggleLock={canToggleLock ? handleLockDashboardToggle : undefined}
isEditing={isEditing}
draft={draft}
onDraftChange={setDraft}

View File

@@ -3,10 +3,10 @@ import type { TagtypesPostableTagDTO } from 'api/generated/services/sigNoz.schem
export { Base64Icons } from 'container/DashboardContainer/DashboardSettings/General/utils';
export { parseKeyValueTag } from 'components/TagKeyValueInput/utils';
// The tag editor is strictly key:value, so always render both sides — a
// `key:key` tag stays `key:key` rather than collapsing to a bare `key`.
// tag UX, a string with no ':' is round-tripped as `{key: x, value: x}` and
// collapsed back to just `x` for display.
export function tagsToStrings(tags: TagtypesPostableTagDTO[]): string[] {
return tags.map((t) => `${t.key}:${t.value}`);
return tags.map((t) => (t.key === t.value ? t.key : `${t.key}:${t.value}`));
}
export function stringsToTags(tagStrings: string[]): TagtypesPostableTagDTO[] {

Some files were not shown because too many files have changed in this diff Show More