mirror of
https://github.com/SigNoz/signoz.git
synced 2026-05-05 09:50:31 +01:00
Compare commits
2 Commits
tvats-dry-
...
issue_4785
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
578e4644e1 | ||
|
|
afee062eaf |
@@ -190,7 +190,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.121.0
|
||||
image: signoz/signoz:v0.120.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
# - "6060:6060" # pprof port
|
||||
|
||||
@@ -117,7 +117,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:v0.121.0
|
||||
image: signoz/signoz:v0.120.0
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
volumes:
|
||||
|
||||
@@ -181,7 +181,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.121.0}
|
||||
image: signoz/signoz:${VERSION:-v0.120.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -109,7 +109,7 @@ services:
|
||||
# - ../common/clickhouse/storage.xml:/etc/clickhouse-server/config.d/storage.xml
|
||||
signoz:
|
||||
!!merge <<: *db-depend
|
||||
image: signoz/signoz:${VERSION:-v0.121.0}
|
||||
image: signoz/signoz:${VERSION:-v0.120.0}
|
||||
container_name: signoz
|
||||
ports:
|
||||
- "8080:8080" # signoz port
|
||||
|
||||
@@ -2632,158 +2632,6 @@ components:
|
||||
- list
|
||||
- grouped_list
|
||||
type: string
|
||||
LlmpricingruletypesGettablePricingRules:
|
||||
properties:
|
||||
items:
|
||||
items:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRule'
|
||||
nullable: true
|
||||
type: array
|
||||
limit:
|
||||
type: integer
|
||||
offset:
|
||||
type: integer
|
||||
total:
|
||||
type: integer
|
||||
required:
|
||||
- items
|
||||
- total
|
||||
- offset
|
||||
- limit
|
||||
type: object
|
||||
LlmpricingruletypesLLMPricingCacheCosts:
|
||||
properties:
|
||||
mode:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleCacheMode'
|
||||
read:
|
||||
format: double
|
||||
type: number
|
||||
write:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- mode
|
||||
type: object
|
||||
LlmpricingruletypesLLMPricingRule:
|
||||
properties:
|
||||
createdAt:
|
||||
format: date-time
|
||||
type: string
|
||||
createdBy:
|
||||
type: string
|
||||
enabled:
|
||||
type: boolean
|
||||
id:
|
||||
type: string
|
||||
isOverride:
|
||||
type: boolean
|
||||
modelName:
|
||||
type: string
|
||||
modelPattern:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesStringSlice'
|
||||
orgId:
|
||||
type: string
|
||||
pricing:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMRulePricing'
|
||||
provider:
|
||||
type: string
|
||||
sourceId:
|
||||
type: string
|
||||
syncedAt:
|
||||
format: date-time
|
||||
nullable: true
|
||||
type: string
|
||||
unit:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleUnit'
|
||||
updatedAt:
|
||||
format: date-time
|
||||
type: string
|
||||
updatedBy:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- orgId
|
||||
- modelName
|
||||
- provider
|
||||
- modelPattern
|
||||
- unit
|
||||
- pricing
|
||||
- isOverride
|
||||
- enabled
|
||||
type: object
|
||||
LlmpricingruletypesLLMPricingRuleCacheMode:
|
||||
enum:
|
||||
- subtract
|
||||
- additive
|
||||
- unknown
|
||||
type: string
|
||||
LlmpricingruletypesLLMPricingRuleUnit:
|
||||
enum:
|
||||
- per_million_tokens
|
||||
type: string
|
||||
LlmpricingruletypesLLMRulePricing:
|
||||
properties:
|
||||
cache:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingCacheCosts'
|
||||
input:
|
||||
format: double
|
||||
type: number
|
||||
output:
|
||||
format: double
|
||||
type: number
|
||||
required:
|
||||
- input
|
||||
- output
|
||||
type: object
|
||||
LlmpricingruletypesStringSlice:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
LlmpricingruletypesUpdatableLLMPricingRule:
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
id:
|
||||
nullable: true
|
||||
type: string
|
||||
isOverride:
|
||||
nullable: true
|
||||
type: boolean
|
||||
modelName:
|
||||
type: string
|
||||
modelPattern:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
pricing:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMRulePricing'
|
||||
provider:
|
||||
type: string
|
||||
sourceId:
|
||||
nullable: true
|
||||
type: string
|
||||
unit:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRuleUnit'
|
||||
required:
|
||||
- modelName
|
||||
- provider
|
||||
- modelPattern
|
||||
- unit
|
||||
- pricing
|
||||
- enabled
|
||||
type: object
|
||||
LlmpricingruletypesUpdatableLLMPricingRules:
|
||||
properties:
|
||||
rules:
|
||||
items:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesUpdatableLLMPricingRule'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- rules
|
||||
type: object
|
||||
MetricsexplorertypesInspectMetricsRequest:
|
||||
properties:
|
||||
end:
|
||||
@@ -7827,218 +7675,6 @@ paths:
|
||||
summary: Create bulk invite
|
||||
tags:
|
||||
- users
|
||||
/api/v1/llm_pricing_rules:
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns all LLM pricing rules for the authenticated org, with pagination.
|
||||
operationId: ListLLMPricingRules
|
||||
parameters:
|
||||
- in: query
|
||||
name: offset
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: limit
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesGettablePricingRules'
|
||||
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 pricing rules
|
||||
tags:
|
||||
- llmpricingrules
|
||||
put:
|
||||
deprecated: false
|
||||
description: Single write endpoint used by both the user and the Zeus sync job.
|
||||
Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true)
|
||||
are fully preserved when the request does not provide isOverride; only synced_at
|
||||
is stamped.
|
||||
operationId: CreateOrUpdateLLMPricingRules
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesUpdatableLLMPricingRules'
|
||||
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
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Create or update pricing rules
|
||||
tags:
|
||||
- llmpricingrules
|
||||
/api/v1/llm_pricing_rules/{id}:
|
||||
delete:
|
||||
deprecated: false
|
||||
description: Hard-deletes a pricing rule. If auto-synced, it will be recreated
|
||||
on the next sync cycle.
|
||||
operationId: DeleteLLMPricingRule
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"204":
|
||||
description: No Content
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- ADMIN
|
||||
summary: Delete a pricing rule
|
||||
tags:
|
||||
- llmpricingrules
|
||||
get:
|
||||
deprecated: false
|
||||
description: Returns a single LLM pricing rule by ID.
|
||||
operationId: GetLLMPricingRule
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/LlmpricingruletypesLLMPricingRule'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Get a pricing rule
|
||||
tags:
|
||||
- llmpricingrules
|
||||
/api/v1/logs/promote_paths:
|
||||
get:
|
||||
deprecated: false
|
||||
@@ -17273,9 +16909,9 @@ paths:
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- ADMIN
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
- ADMIN
|
||||
summary: Get host info from Zeus.
|
||||
tags:
|
||||
- zeus
|
||||
|
||||
@@ -105,10 +105,6 @@ func (h *handler) QueryRawStream(rw http.ResponseWriter, req *http.Request) {
|
||||
h.community.QueryRawStream(rw, req)
|
||||
}
|
||||
|
||||
func (h *handler) QueryRangePreview(rw http.ResponseWriter, req *http.Request) {
|
||||
h.community.QueryRangePreview(rw, req)
|
||||
}
|
||||
|
||||
func (h *handler) ReplaceVariables(rw http.ResponseWriter, req *http.Request) {
|
||||
h.community.ReplaceVariables(rw, req)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/icons": "0.1.0",
|
||||
"@signozhq/resizable": "0.0.2",
|
||||
"@signozhq/ui": "0.0.12",
|
||||
"@signozhq/ui": "0.0.10",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
|
||||
@@ -1,399 +0,0 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'yarn generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import { useMutation, useQuery } from 'react-query';
|
||||
import type {
|
||||
InvalidateOptions,
|
||||
MutationFunction,
|
||||
QueryClient,
|
||||
QueryFunction,
|
||||
QueryKey,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
UseQueryOptions,
|
||||
UseQueryResult,
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
DeleteLLMPricingRulePathParameters,
|
||||
GetLLMPricingRule200,
|
||||
GetLLMPricingRulePathParameters,
|
||||
ListLLMPricingRules200,
|
||||
ListLLMPricingRulesParams,
|
||||
LlmpricingruletypesUpdatableLLMPricingRulesDTO,
|
||||
RenderErrorResponseDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* Returns all LLM pricing rules for the authenticated org, with pagination.
|
||||
* @summary List pricing rules
|
||||
*/
|
||||
export const listLLMPricingRules = (
|
||||
params?: ListLLMPricingRulesParams,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<ListLLMPricingRules200>({
|
||||
url: `/api/v1/llm_pricing_rules`,
|
||||
method: 'GET',
|
||||
params,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getListLLMPricingRulesQueryKey = (
|
||||
params?: ListLLMPricingRulesParams,
|
||||
) => {
|
||||
return [`/api/v1/llm_pricing_rules`, ...(params ? [params] : [])] as const;
|
||||
};
|
||||
|
||||
export const getListLLMPricingRulesQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListLLMPricingRulesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getListLLMPricingRulesQueryKey(params);
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>
|
||||
> = ({ signal }) => listLLMPricingRules(params, signal);
|
||||
|
||||
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type ListLLMPricingRulesQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>
|
||||
>;
|
||||
export type ListLLMPricingRulesQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary List pricing rules
|
||||
*/
|
||||
|
||||
export function useListLLMPricingRules<
|
||||
TData = Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
params?: ListLLMPricingRulesParams,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof listLLMPricingRules>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getListLLMPricingRulesQueryOptions(params, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary List pricing rules
|
||||
*/
|
||||
export const invalidateListLLMPricingRules = async (
|
||||
queryClient: QueryClient,
|
||||
params?: ListLLMPricingRulesParams,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getListLLMPricingRulesQueryKey(params) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Single write endpoint used by both the user and the Zeus sync job. Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true) are fully preserved when the request does not provide isOverride; only synced_at is stamped.
|
||||
* @summary Create or update pricing rules
|
||||
*/
|
||||
export const createOrUpdateLLMPricingRules = (
|
||||
llmpricingruletypesUpdatableLLMPricingRulesDTO: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO>,
|
||||
) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/llm_pricing_rules`,
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: llmpricingruletypesUpdatableLLMPricingRulesDTO,
|
||||
});
|
||||
};
|
||||
|
||||
export const getCreateOrUpdateLLMPricingRulesMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
|
||||
TError,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
|
||||
TError,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['createOrUpdateLLMPricingRules'];
|
||||
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 createOrUpdateLLMPricingRules>>,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return createOrUpdateLLMPricingRules(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type CreateOrUpdateLLMPricingRulesMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>
|
||||
>;
|
||||
export type CreateOrUpdateLLMPricingRulesMutationBody =
|
||||
BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO>;
|
||||
export type CreateOrUpdateLLMPricingRulesMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Create or update pricing rules
|
||||
*/
|
||||
export const useCreateOrUpdateLLMPricingRules = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
|
||||
TError,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof createOrUpdateLLMPricingRules>>,
|
||||
TError,
|
||||
{ data: BodyType<LlmpricingruletypesUpdatableLLMPricingRulesDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions =
|
||||
getCreateOrUpdateLLMPricingRulesMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Hard-deletes a pricing rule. If auto-synced, it will be recreated on the next sync cycle.
|
||||
* @summary Delete a pricing rule
|
||||
*/
|
||||
export const deleteLLMPricingRule = ({
|
||||
id,
|
||||
}: DeleteLLMPricingRulePathParameters) => {
|
||||
return GeneratedAPIInstance<void>({
|
||||
url: `/api/v1/llm_pricing_rules/${id}`,
|
||||
method: 'DELETE',
|
||||
});
|
||||
};
|
||||
|
||||
export const getDeleteLLMPricingRuleMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['deleteLLMPricingRule'];
|
||||
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 deleteLLMPricingRule>>,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters }
|
||||
> = (props) => {
|
||||
const { pathParams } = props ?? {};
|
||||
|
||||
return deleteLLMPricingRule(pathParams);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type DeleteLLMPricingRuleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>
|
||||
>;
|
||||
|
||||
export type DeleteLLMPricingRuleMutationError =
|
||||
ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Delete a pricing rule
|
||||
*/
|
||||
export const useDeleteLLMPricingRule = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof deleteLLMPricingRule>>,
|
||||
TError,
|
||||
{ pathParams: DeleteLLMPricingRulePathParameters },
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getDeleteLLMPricingRuleMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
/**
|
||||
* Returns a single LLM pricing rule by ID.
|
||||
* @summary Get a pricing rule
|
||||
*/
|
||||
export const getLLMPricingRule = (
|
||||
{ id }: GetLLMPricingRulePathParameters,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetLLMPricingRule200>({
|
||||
url: `/api/v1/llm_pricing_rules/${id}`,
|
||||
method: 'GET',
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetLLMPricingRuleQueryKey = ({
|
||||
id,
|
||||
}: GetLLMPricingRulePathParameters) => {
|
||||
return [`/api/v1/llm_pricing_rules/${id}`] as const;
|
||||
};
|
||||
|
||||
export const getGetLLMPricingRuleQueryOptions = <
|
||||
TData = Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetLLMPricingRulePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
) => {
|
||||
const { query: queryOptions } = options ?? {};
|
||||
|
||||
const queryKey =
|
||||
queryOptions?.queryKey ?? getGetLLMPricingRuleQueryKey({ id });
|
||||
|
||||
const queryFn: QueryFunction<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>
|
||||
> = ({ signal }) => getLLMPricingRule({ id }, signal);
|
||||
|
||||
return {
|
||||
queryKey,
|
||||
queryFn,
|
||||
enabled: !!id,
|
||||
...queryOptions,
|
||||
} as UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError,
|
||||
TData
|
||||
> & { queryKey: QueryKey };
|
||||
};
|
||||
|
||||
export type GetLLMPricingRuleQueryResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>
|
||||
>;
|
||||
export type GetLLMPricingRuleQueryError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get a pricing rule
|
||||
*/
|
||||
|
||||
export function useGetLLMPricingRule<
|
||||
TData = Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
>(
|
||||
{ id }: GetLLMPricingRulePathParameters,
|
||||
options?: {
|
||||
query?: UseQueryOptions<
|
||||
Awaited<ReturnType<typeof getLLMPricingRule>>,
|
||||
TError,
|
||||
TData
|
||||
>;
|
||||
},
|
||||
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
|
||||
const queryOptions = getGetLLMPricingRuleQueryOptions({ id }, options);
|
||||
|
||||
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
|
||||
queryKey: QueryKey;
|
||||
};
|
||||
|
||||
query.queryKey = queryOptions.queryKey;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Get a pricing rule
|
||||
*/
|
||||
export const invalidateGetLLMPricingRule = async (
|
||||
queryClient: QueryClient,
|
||||
{ id }: GetLLMPricingRulePathParameters,
|
||||
options?: InvalidateOptions,
|
||||
): Promise<QueryClient> => {
|
||||
await queryClient.invalidateQueries(
|
||||
{ queryKey: getGetLLMPricingRuleQueryKey({ id }) },
|
||||
options,
|
||||
);
|
||||
|
||||
return queryClient;
|
||||
};
|
||||
@@ -3413,170 +3413,6 @@ export enum InframonitoringtypesResponseTypeDTO {
|
||||
list = 'list',
|
||||
grouped_list = 'grouped_list',
|
||||
}
|
||||
export interface LlmpricingruletypesGettablePricingRulesDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
items: LlmpricingruletypesLLMPricingRuleDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
limit: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
offset: number;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface LlmpricingruletypesLLMPricingCacheCostsDTO {
|
||||
mode: LlmpricingruletypesLLMPricingRuleCacheModeDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
read?: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
write?: number;
|
||||
}
|
||||
|
||||
export interface LlmpricingruletypesLLMPricingRuleDTO {
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
createdAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
createdBy?: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
isOverride: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
modelName: string;
|
||||
modelPattern: LlmpricingruletypesStringSliceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
orgId: string;
|
||||
pricing: LlmpricingruletypesLLMRulePricingDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
provider: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
sourceId?: string;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
* @nullable true
|
||||
*/
|
||||
syncedAt?: Date | null;
|
||||
unit: LlmpricingruletypesLLMPricingRuleUnitDTO;
|
||||
/**
|
||||
* @type string
|
||||
* @format date-time
|
||||
*/
|
||||
updatedAt?: Date;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
updatedBy?: string;
|
||||
}
|
||||
|
||||
export enum LlmpricingruletypesLLMPricingRuleCacheModeDTO {
|
||||
subtract = 'subtract',
|
||||
additive = 'additive',
|
||||
unknown = 'unknown',
|
||||
}
|
||||
export enum LlmpricingruletypesLLMPricingRuleUnitDTO {
|
||||
per_million_tokens = 'per_million_tokens',
|
||||
}
|
||||
export interface LlmpricingruletypesLLMRulePricingDTO {
|
||||
cache?: LlmpricingruletypesLLMPricingCacheCostsDTO;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
input: number;
|
||||
/**
|
||||
* @type number
|
||||
* @format double
|
||||
*/
|
||||
output: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type LlmpricingruletypesStringSliceDTO = string[] | null;
|
||||
|
||||
export interface LlmpricingruletypesUpdatableLLMPricingRuleDTO {
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled: boolean;
|
||||
/**
|
||||
* @type string
|
||||
* @nullable true
|
||||
*/
|
||||
id?: string | null;
|
||||
/**
|
||||
* @type boolean
|
||||
* @nullable true
|
||||
*/
|
||||
isOverride?: boolean | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
modelName: string;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
modelPattern: string[] | null;
|
||||
pricing: LlmpricingruletypesLLMRulePricingDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
provider: string;
|
||||
/**
|
||||
* @type string
|
||||
* @nullable true
|
||||
*/
|
||||
sourceId?: string | null;
|
||||
unit: LlmpricingruletypesLLMPricingRuleUnitDTO;
|
||||
}
|
||||
|
||||
export interface LlmpricingruletypesUpdatableLLMPricingRulesDTO {
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
rules: LlmpricingruletypesUpdatableLLMPricingRuleDTO[] | null;
|
||||
}
|
||||
|
||||
export interface MetricsexplorertypesInspectMetricsRequestDTO {
|
||||
/**
|
||||
* @type integer
|
||||
@@ -7168,41 +7004,6 @@ export type CreateInvite201 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListLLMPricingRulesParams = {
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
offset?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @description undefined
|
||||
*/
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
export type ListLLMPricingRules200 = {
|
||||
data: LlmpricingruletypesGettablePricingRulesDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type DeleteLLMPricingRulePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetLLMPricingRulePathParameters = {
|
||||
id: string;
|
||||
};
|
||||
export type GetLLMPricingRule200 = {
|
||||
data: LlmpricingruletypesLLMPricingRuleDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type ListPromotedAndIndexedPaths200 = {
|
||||
/**
|
||||
* @type array
|
||||
|
||||
@@ -46,7 +46,6 @@ function DeleteMemberDialog({
|
||||
color="destructive"
|
||||
disabled={isDeleting}
|
||||
onClick={onConfirm}
|
||||
loading={isDeleting}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
{isDeleting ? 'Processing...' : title}
|
||||
@@ -64,6 +63,7 @@ function DeleteMemberDialog({
|
||||
}}
|
||||
title={title}
|
||||
width="narrow"
|
||||
className="alert-dialog delete-dialog"
|
||||
showCloseButton={false}
|
||||
disableOutsideClick={false}
|
||||
footer={footer}
|
||||
|
||||
@@ -28,6 +28,18 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&__input {
|
||||
height: 32px;
|
||||
background: var(--l2-background);
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
box-shadow: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -36,7 +48,7 @@
|
||||
padding: var(--padding-1) var(--padding-2);
|
||||
border-radius: 2px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--border);
|
||||
border: 1px solid var(--l1-border);
|
||||
box-sizing: border-box;
|
||||
|
||||
&--disabled {
|
||||
@@ -53,8 +65,8 @@
|
||||
}
|
||||
|
||||
&__email-text {
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
color: var(--foreground);
|
||||
line-height: var(--line-height-18);
|
||||
letter-spacing: -0.07px;
|
||||
@@ -166,6 +178,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
.delete-dialog {
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
|
||||
[data-slot='dialog-title'] {
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
&__body {
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
color: var(--l2-foreground);
|
||||
line-height: var(--paragraph-base-400-line-height);
|
||||
letter-spacing: -0.065px;
|
||||
margin: 0;
|
||||
|
||||
strong {
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--spacing-4);
|
||||
margin-top: var(--margin-6);
|
||||
}
|
||||
}
|
||||
|
||||
.reset-link-dialog {
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
@@ -222,6 +264,13 @@
|
||||
}
|
||||
|
||||
&__copy-btn {
|
||||
flex-shrink: 0;
|
||||
height: 32px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
border-left: 1px solid var(--l1-border);
|
||||
min-width: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ function EditMemberDrawer({
|
||||
try {
|
||||
await rawRetry();
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== context));
|
||||
void refetchUser();
|
||||
refetchUser();
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
@@ -250,7 +250,7 @@ function EditMemberDrawer({
|
||||
});
|
||||
}
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update'));
|
||||
void refetchUser();
|
||||
refetchUser();
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
@@ -319,7 +319,7 @@ function EditMemberDrawer({
|
||||
}),
|
||||
];
|
||||
});
|
||||
void refetchUser();
|
||||
refetchUser();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -340,7 +340,7 @@ function EditMemberDrawer({
|
||||
onComplete();
|
||||
}
|
||||
|
||||
void refetchUser();
|
||||
refetchUser();
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -465,6 +465,7 @@ function EditMemberDrawer({
|
||||
prev.filter((err) => err.context !== 'Name update'),
|
||||
);
|
||||
}}
|
||||
className="edit-member-drawer__input"
|
||||
placeholder="Enter name"
|
||||
disabled={isRootUser || isDeleted}
|
||||
/>
|
||||
@@ -630,7 +631,7 @@ function EditMemberDrawer({
|
||||
</div>
|
||||
|
||||
<div className="edit-member-drawer__footer-right">
|
||||
<Button variant="outlined" color="secondary" onClick={handleClose}>
|
||||
<Button variant="solid" color="secondary" onClick={handleClose}>
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>
|
||||
@@ -640,7 +641,6 @@ function EditMemberDrawer({
|
||||
color="primary"
|
||||
disabled={!isDirty || isSaving || isRootUser}
|
||||
onClick={handleSave}
|
||||
loading={isSaving}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Member Details'}
|
||||
</Button>
|
||||
|
||||
@@ -44,8 +44,9 @@ function ResetLinkDialog({
|
||||
<span className="reset-link-dialog__link-text">{resetLink}</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="link"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={onCopy}
|
||||
prefix={hasCopied ? <Check size={12} /> : <Copy size={12} />}
|
||||
className="reset-link-dialog__copy-btn"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Style } from '@signozhq/design-tokens';
|
||||
import { ChevronDown, Plus, Trash2, X } from '@signozhq/icons';
|
||||
import { ChevronDown, CircleAlert, Plus, Trash2, X } from '@signozhq/icons';
|
||||
import {
|
||||
Button,
|
||||
Callout,
|
||||
@@ -294,8 +294,10 @@ function InviteMembersModal({
|
||||
type="error"
|
||||
size="small"
|
||||
showIcon
|
||||
title={getValidationErrorMessage()}
|
||||
/>
|
||||
icon={<CircleAlert size={12} />}
|
||||
>
|
||||
{getValidationErrorMessage()}
|
||||
</Callout>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
|
||||
input {
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.ant-picker-suffix {
|
||||
@@ -126,6 +126,12 @@
|
||||
}
|
||||
|
||||
&__copy-btn {
|
||||
flex-shrink: 0;
|
||||
height: 32px;
|
||||
border-radius: 0 2px 2px 0;
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
border-left: 1px solid var(--l1-border);
|
||||
min-width: 40px;
|
||||
}
|
||||
@@ -146,7 +152,6 @@
|
||||
color: var(--foreground);
|
||||
letter-spacing: 0.48px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
|
||||
&__footer {
|
||||
|
||||
@@ -22,8 +22,9 @@ function KeyCreatedPhase({
|
||||
<div className="add-key-modal__key-display">
|
||||
<span className="add-key-modal__key-text">{createdKey.key}</span>
|
||||
<Button
|
||||
variant="link"
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={onCopy}
|
||||
className="add-key-modal__copy-btn"
|
||||
>
|
||||
|
||||
@@ -106,7 +106,7 @@ function KeyFormPhase({
|
||||
|
||||
<div className="add-key-modal__footer">
|
||||
<div className="add-key-modal__footer-right">
|
||||
<Button variant="solid" color="secondary" onClick={onClose}>
|
||||
<Button variant="solid" color="secondary" size="sm" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
@@ -115,6 +115,7 @@ function KeyFormPhase({
|
||||
form={FORM_ID}
|
||||
variant="solid"
|
||||
color="primary"
|
||||
size="sm"
|
||||
loading={isSubmitting}
|
||||
disabled={!isValid}
|
||||
>
|
||||
|
||||
@@ -136,7 +136,7 @@ function EditKeyForm({
|
||||
</form>
|
||||
|
||||
<div className="edit-key-modal__footer">
|
||||
<Button variant="link" color="destructive" onClick={onRevokeClick}>
|
||||
<Button variant="ghost" color="destructive" onClick={onRevokeClick}>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
</Button>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
input {
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-xs);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.ant-picker-suffix {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
import { RevokeKeyFooter } from '../RevokeKeyModal';
|
||||
import { RevokeKeyContent } from '../RevokeKeyModal';
|
||||
import EditKeyForm from './EditKeyForm';
|
||||
import type { FormValues } from './types';
|
||||
import { DEFAULT_FORM_VALUES, ExpiryMode } from './types';
|
||||
@@ -158,25 +158,17 @@ function EditKeyModal({ keyItem }: EditKeyModalProps): JSX.Element {
|
||||
}
|
||||
width={isRevokeConfirmOpen ? 'narrow' : 'base'}
|
||||
className={
|
||||
isRevokeConfirmOpen ? 'alert-dialog sa-delete-dialog' : 'edit-key-modal'
|
||||
isRevokeConfirmOpen ? 'alert-dialog delete-dialog' : 'edit-key-modal'
|
||||
}
|
||||
showCloseButton={!isRevokeConfirmOpen}
|
||||
disableOutsideClick={isErrorModalVisible}
|
||||
footer={
|
||||
isRevokeConfirmOpen ? (
|
||||
<RevokeKeyFooter
|
||||
isRevoking={isRevoking}
|
||||
onCancel={(): void => setIsRevokeConfirmOpen(false)}
|
||||
onConfirm={handleRevoke}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{isRevokeConfirmOpen ? (
|
||||
<>
|
||||
Revoking this key will permanently invalidate it. Any systems using this
|
||||
key will lose access immediately.
|
||||
</>
|
||||
<RevokeKeyContent
|
||||
isRevoking={isRevoking}
|
||||
onCancel={(): void => setIsRevokeConfirmOpen(false)}
|
||||
onConfirm={handleRevoke}
|
||||
/>
|
||||
) : (
|
||||
<EditKeyForm
|
||||
register={register}
|
||||
|
||||
@@ -72,6 +72,7 @@ function OverviewTab({
|
||||
id="sa-name"
|
||||
value={localName}
|
||||
onChange={(e): void => onNameChange(e.target.value)}
|
||||
className="sa-drawer__input"
|
||||
placeholder="Enter name"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -17,32 +17,39 @@ import { parseAsString, useQueryState } from 'nuqs';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
|
||||
export interface RevokeKeyFooterProps {
|
||||
export interface RevokeKeyContentProps {
|
||||
isRevoking: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export function RevokeKeyFooter({
|
||||
export function RevokeKeyContent({
|
||||
isRevoking,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: RevokeKeyFooterProps): JSX.Element {
|
||||
}: RevokeKeyContentProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Button variant="solid" color="secondary" onClick={onCancel}>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
loading={isRevoking}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
</Button>
|
||||
<p className="delete-dialog__body">
|
||||
Revoking this key will permanently invalidate it. Any systems using this key
|
||||
will lose access immediately.
|
||||
</p>
|
||||
<div className="delete-dialog__footer">
|
||||
<Button variant="solid" color="secondary" size="sm" onClick={onCancel}>
|
||||
<X size={12} />
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="destructive"
|
||||
size="sm"
|
||||
loading={isRevoking}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
Revoke Key
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -105,19 +112,15 @@ function RevokeKeyModal(): JSX.Element {
|
||||
}}
|
||||
title={`Revoke ${keyName ?? 'key'}?`}
|
||||
width="narrow"
|
||||
className="alert-dialog sa-delete-dialog"
|
||||
className="alert-dialog delete-dialog"
|
||||
showCloseButton={false}
|
||||
disableOutsideClick={isErrorModalVisible}
|
||||
footer={
|
||||
<RevokeKeyFooter
|
||||
isRevoking={isRevoking}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Revoking this key will permanently invalidate it. Any systems using this key
|
||||
will lose access immediately.
|
||||
<RevokeKeyContent
|
||||
isRevoking={isRevoking}
|
||||
onCancel={handleCancel}
|
||||
onConfirm={handleConfirm}
|
||||
/>
|
||||
</DialogWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,8 +57,6 @@
|
||||
color: var(--l1-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
&__tab {
|
||||
@@ -168,6 +166,18 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&__input {
|
||||
height: 32px;
|
||||
background: var(--l2-background);
|
||||
border-color: var(--l1-border);
|
||||
color: var(--l1-foreground);
|
||||
box-shadow: none;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
&__input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -176,7 +186,7 @@
|
||||
padding: 0 var(--padding-2);
|
||||
border-radius: 2px;
|
||||
background: var(--l2-background);
|
||||
border: 1px solid var(--border);
|
||||
border: 1px solid var(--l1-border);
|
||||
|
||||
&--disabled {
|
||||
cursor: not-allowed;
|
||||
@@ -185,8 +195,8 @@
|
||||
}
|
||||
|
||||
&__input-text {
|
||||
font-size: var(--paragraph-base-400-font-size);
|
||||
font-weight: var(--paragraph-base-400-font-weight);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-normal);
|
||||
color: var(--foreground);
|
||||
line-height: var(--line-height-18);
|
||||
letter-spacing: -0.07px;
|
||||
|
||||
@@ -129,7 +129,7 @@ function ServiceAccountDrawer({
|
||||
useEffect(() => {
|
||||
if (account?.id) {
|
||||
setLocalName(account?.name ?? '');
|
||||
void setKeysPage(1);
|
||||
setKeysPage(1);
|
||||
}
|
||||
}, [account?.id, account?.name, setKeysPage]);
|
||||
|
||||
@@ -176,7 +176,7 @@ function ServiceAccountDrawer({
|
||||
}
|
||||
const maxPage = Math.max(1, Math.ceil(keys.length / PAGE_SIZE));
|
||||
if (keysPage > maxPage) {
|
||||
void setKeysPage(maxPage);
|
||||
setKeysPage(maxPage);
|
||||
}
|
||||
}, [keysLoading, keys.length, keysPage, setKeysPage]);
|
||||
|
||||
@@ -214,8 +214,8 @@ function ServiceAccountDrawer({
|
||||
data: { name: localName },
|
||||
});
|
||||
setSaveErrors((prev) => prev.filter((e) => e.context !== 'Name update'));
|
||||
void refetchAccount();
|
||||
void queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
refetchAccount();
|
||||
queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
} catch (err) {
|
||||
setSaveErrors((prev) =>
|
||||
prev.map((e) =>
|
||||
@@ -337,8 +337,8 @@ function ServiceAccountDrawer({
|
||||
onSuccess({ closeDrawer: false });
|
||||
}
|
||||
|
||||
void refetchAccount();
|
||||
void queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
refetchAccount();
|
||||
queryClient.invalidateQueries(getListServiceAccountsQueryKey());
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -357,12 +357,12 @@ function ServiceAccountDrawer({
|
||||
]);
|
||||
|
||||
const handleClose = useCallback((): void => {
|
||||
void setIsDeleteOpen(null);
|
||||
void setIsAddKeyOpen(null);
|
||||
void setSelectedAccountId(null);
|
||||
void setActiveTab(null);
|
||||
void setKeysPage(null);
|
||||
void setEditKeyId(null);
|
||||
setIsDeleteOpen(null);
|
||||
setIsAddKeyOpen(null);
|
||||
setSelectedAccountId(null);
|
||||
setActiveTab(null);
|
||||
setKeysPage(null);
|
||||
setEditKeyId(null);
|
||||
setSaveErrors([]);
|
||||
}, [
|
||||
setSelectedAccountId,
|
||||
@@ -379,13 +379,12 @@ function ServiceAccountDrawer({
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={activeTab}
|
||||
size="sm"
|
||||
onChange={(val): void => {
|
||||
if (val) {
|
||||
void setActiveTab(val as ServiceAccountDrawerTab);
|
||||
setActiveTab(val as ServiceAccountDrawerTab);
|
||||
if (val !== ServiceAccountDrawerTab.Keys) {
|
||||
void setKeysPage(null);
|
||||
void setEditKeyId(null);
|
||||
setKeysPage(null);
|
||||
setEditKeyId(null);
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -416,7 +415,7 @@ function ServiceAccountDrawer({
|
||||
color="secondary"
|
||||
disabled={isDeleted}
|
||||
onClick={(): void => {
|
||||
void setIsAddKeyOpen(true);
|
||||
setIsAddKeyOpen(true);
|
||||
}}
|
||||
>
|
||||
<Plus size={12} />
|
||||
@@ -504,7 +503,7 @@ function ServiceAccountDrawer({
|
||||
variant="link"
|
||||
color="destructive"
|
||||
onClick={(): void => {
|
||||
void setIsDeleteOpen(true);
|
||||
setIsDeleteOpen(true);
|
||||
}}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
@@ -513,7 +512,7 @@ function ServiceAccountDrawer({
|
||||
)}
|
||||
{!isDeleted && (
|
||||
<div className="sa-drawer__footer-right">
|
||||
<Button variant="outlined" color="secondary" onClick={handleClose}>
|
||||
<Button variant="solid" color="secondary" onClick={handleClose}>
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -78,7 +78,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-10);
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
.custom-domain-card-meta-row.workspace-name-hidden {
|
||||
@@ -125,6 +124,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-url-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3);
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
color: var(--l1-foreground);
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-18);
|
||||
letter-spacing: -0.06px;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: var(--l2-foreground);
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-url-dropdown {
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--l1-border);
|
||||
|
||||
@@ -204,7 +204,6 @@ export default function CustomDomainSettings(): JSX.Element {
|
||||
>
|
||||
<Dropdown
|
||||
trigger={['click']}
|
||||
disabled={isFetchingHosts}
|
||||
dropdownRender={(): JSX.Element => (
|
||||
<div className="workspace-url-dropdown">
|
||||
<span className="workspace-url-dropdown-header">
|
||||
@@ -240,7 +239,12 @@ export default function CustomDomainSettings(): JSX.Element {
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<Button variant="link" color="none">
|
||||
<Button
|
||||
className="workspace-url-trigger"
|
||||
disabled={isFetchingHosts}
|
||||
variant="link"
|
||||
color="none"
|
||||
>
|
||||
<Link2 size={12} />
|
||||
<span>{stripProtocol(activeHost?.url ?? '')}</span>
|
||||
<ChevronDown size={12} />
|
||||
|
||||
@@ -14,7 +14,6 @@ import { usePanelContextMenu } from '../../hooks/usePanelContextMenu';
|
||||
import { prepareBarPanelConfig, prepareBarPanelData } from './utils';
|
||||
|
||||
import '../Panel.styles.scss';
|
||||
import get from 'lodash/get';
|
||||
|
||||
function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
const {
|
||||
@@ -115,7 +114,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
|
||||
}, []);
|
||||
|
||||
const groupBy = useMemo(() => {
|
||||
return get(widget, 'query.builder.queryData[0].groupBy', []);
|
||||
return widget.query.builder.queryData[0].groupBy;
|
||||
}, [widget.query]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -10,7 +10,6 @@ import { ContextMenu } from 'periscope/components/ContextMenu';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import uPlot from 'uplot';
|
||||
import { getTimeRange } from 'utils/getTimeRange';
|
||||
import get from 'lodash/get';
|
||||
|
||||
import { prepareChartData, prepareUPlotConfig } from '../TimeSeriesPanel/utils';
|
||||
|
||||
@@ -106,7 +105,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
|
||||
]);
|
||||
|
||||
const groupBy = useMemo(() => {
|
||||
return get(widget, 'query.builder.queryData[0].groupBy', []);
|
||||
return widget.query.builder.queryData[0].groupBy;
|
||||
}, [widget.query]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -67,40 +67,4 @@ describe('AuthCard', () => {
|
||||
|
||||
expect(mockOnCreateServiceAccount).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows URL for non-admin (all roles can fetch instance URL)', () => {
|
||||
render(<AuthCard {...defaultProps} isAdmin={false} />);
|
||||
|
||||
expect(screen.getByTestId('mcp-instance-url')).toHaveTextContent(
|
||||
'http://localhost',
|
||||
);
|
||||
});
|
||||
|
||||
describe('isLoadingInstanceUrl', () => {
|
||||
it('shows a skeleton and hides the URL while loading', () => {
|
||||
render(<AuthCard {...defaultProps} isAdmin isLoadingInstanceUrl />);
|
||||
|
||||
expect(screen.queryByTestId('mcp-instance-url')).not.toBeInTheDocument();
|
||||
expect(document.querySelector('.ant-skeleton-input')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render the copy button while loading', () => {
|
||||
render(<AuthCard {...defaultProps} isAdmin isLoadingInstanceUrl />);
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Copy SigNoz instance URL' }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the URL and copy button once loading is done', () => {
|
||||
render(<AuthCard {...defaultProps} isAdmin isLoadingInstanceUrl={false} />);
|
||||
|
||||
expect(screen.getByTestId('mcp-instance-url')).toHaveTextContent(
|
||||
'http://localhost',
|
||||
);
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Copy SigNoz instance URL' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Skeleton } from 'antd';
|
||||
import { Badge, Button } from '@signozhq/ui';
|
||||
import { Info, KeyRound } from '@signozhq/icons';
|
||||
import CopyIconButton from '../CopyIconButton';
|
||||
@@ -8,7 +7,6 @@ import './AuthCard.styles.scss';
|
||||
interface AuthCardProps {
|
||||
isAdmin: boolean;
|
||||
instanceUrl: string;
|
||||
isLoadingInstanceUrl?: boolean;
|
||||
onCopyInstanceUrl: () => void;
|
||||
onCreateServiceAccount: () => void;
|
||||
}
|
||||
@@ -16,7 +14,6 @@ interface AuthCardProps {
|
||||
function AuthCard({
|
||||
isAdmin,
|
||||
instanceUrl,
|
||||
isLoadingInstanceUrl = false,
|
||||
onCopyInstanceUrl,
|
||||
onCreateServiceAccount,
|
||||
}: AuthCardProps): JSX.Element {
|
||||
@@ -35,18 +32,13 @@ function AuthCard({
|
||||
|
||||
<div className="mcp-auth-card__field">
|
||||
<span className="mcp-auth-card__field-label">SigNoz Instance URL</span>
|
||||
{isLoadingInstanceUrl ? (
|
||||
<Skeleton.Input active size="small" />
|
||||
) : (
|
||||
<div className="mcp-auth-card__endpoint-value">
|
||||
<span data-testid="mcp-instance-url">{instanceUrl}</span>
|
||||
<CopyIconButton
|
||||
ariaLabel="Copy SigNoz instance URL"
|
||||
onCopy={onCopyInstanceUrl}
|
||||
disabled={isLoadingInstanceUrl}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mcp-auth-card__endpoint-value">
|
||||
<span data-testid="mcp-instance-url">{instanceUrl}</span>
|
||||
<CopyIconButton
|
||||
ariaLabel="Copy SigNoz instance URL"
|
||||
onCopy={onCopyInstanceUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mcp-auth-card__field">
|
||||
|
||||
@@ -6,8 +6,6 @@ const mockLogEvent = jest.fn();
|
||||
const mockCopyToClipboard = jest.fn();
|
||||
const mockHistoryPush = jest.fn();
|
||||
const mockUseGetGlobalConfig = jest.fn();
|
||||
const mockUseGetHosts = jest.fn();
|
||||
const mockUseGetTenantLicense = jest.fn();
|
||||
const mockToastSuccess = jest.fn();
|
||||
const mockToastWarning = jest.fn();
|
||||
|
||||
@@ -21,14 +19,6 @@ jest.mock('api/generated/services/global', () => ({
|
||||
mockUseGetGlobalConfig(...args),
|
||||
}));
|
||||
|
||||
jest.mock('api/generated/services/zeus', () => ({
|
||||
useGetHosts: (...args: unknown[]): unknown => mockUseGetHosts(...args),
|
||||
}));
|
||||
|
||||
jest.mock('hooks/useGetTenantLicense', () => ({
|
||||
useGetTenantLicense: (): unknown => mockUseGetTenantLicense(),
|
||||
}));
|
||||
|
||||
jest.mock('react-use', () => ({
|
||||
__esModule: true,
|
||||
useCopyToClipboard: (): [unknown, jest.Mock] => [null, mockCopyToClipboard],
|
||||
@@ -57,23 +47,6 @@ jest.mock('utils/basePath', () => ({
|
||||
}));
|
||||
|
||||
const MCP_URL = 'https://mcp.us.signoz.cloud/mcp';
|
||||
const CUSTOM_HOST_URL = 'https://myteam.signoz.cloud';
|
||||
const DEFAULT_HOST_URL = 'https://default.signoz.cloud';
|
||||
|
||||
function setupLicense({
|
||||
isCloudUser = true,
|
||||
isEnterpriseSelfHostedUser = false,
|
||||
}: {
|
||||
isCloudUser?: boolean;
|
||||
isEnterpriseSelfHostedUser?: boolean;
|
||||
} = {}): void {
|
||||
mockUseGetTenantLicense.mockReturnValue({
|
||||
isCloudUser,
|
||||
isEnterpriseSelfHostedUser,
|
||||
isCommunityUser: !isCloudUser && !isEnterpriseSelfHostedUser,
|
||||
isCommunityEnterpriseUser: false,
|
||||
});
|
||||
}
|
||||
|
||||
function setupGlobalConfig({ mcpUrl }: { mcpUrl: string | null }): void {
|
||||
mockUseGetGlobalConfig.mockReturnValue({
|
||||
@@ -82,29 +55,7 @@ function setupGlobalConfig({ mcpUrl }: { mcpUrl: string | null }): void {
|
||||
});
|
||||
}
|
||||
|
||||
function setupHosts({
|
||||
hosts = [],
|
||||
isLoading = false,
|
||||
isError = false,
|
||||
}: {
|
||||
hosts?: { name?: string; url?: string; is_default?: boolean }[];
|
||||
isLoading?: boolean;
|
||||
isError?: boolean;
|
||||
} = {}): void {
|
||||
mockUseGetHosts.mockReturnValue({
|
||||
data: isLoading || isError ? undefined : { data: { hosts } },
|
||||
isLoading,
|
||||
isError,
|
||||
});
|
||||
}
|
||||
|
||||
describe('MCPServerSettings', () => {
|
||||
beforeEach(() => {
|
||||
// Default: cloud user, hosts loaded but empty → instanceUrl falls back to getBaseUrl()
|
||||
setupLicense();
|
||||
setupHosts();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -207,145 +158,4 @@ describe('MCPServerSettings', () => {
|
||||
'Instance URL copied to clipboard',
|
||||
);
|
||||
});
|
||||
|
||||
describe('instance URL resolution', () => {
|
||||
it('uses the active custom host URL when available', async () => {
|
||||
setupGlobalConfig({ mcpUrl: MCP_URL });
|
||||
setupHosts({
|
||||
hosts: [
|
||||
{ name: 'default', url: DEFAULT_HOST_URL, is_default: true },
|
||||
{ name: 'myteam', url: CUSTOM_HOST_URL, is_default: false },
|
||||
],
|
||||
});
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<MCPServerSettings />);
|
||||
|
||||
expect(screen.getByTestId('mcp-instance-url')).toHaveTextContent(
|
||||
CUSTOM_HOST_URL,
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Copy SigNoz instance URL' }),
|
||||
);
|
||||
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith(CUSTOM_HOST_URL);
|
||||
});
|
||||
|
||||
it('falls back to the default host URL when no custom host exists', async () => {
|
||||
setupGlobalConfig({ mcpUrl: MCP_URL });
|
||||
setupHosts({
|
||||
hosts: [{ name: 'default', url: DEFAULT_HOST_URL, is_default: true }],
|
||||
});
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<MCPServerSettings />);
|
||||
|
||||
expect(screen.getByTestId('mcp-instance-url')).toHaveTextContent(
|
||||
DEFAULT_HOST_URL,
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Copy SigNoz instance URL' }),
|
||||
);
|
||||
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith(DEFAULT_HOST_URL);
|
||||
});
|
||||
|
||||
it('falls back to browser URL when hosts request errors', async () => {
|
||||
setupGlobalConfig({ mcpUrl: MCP_URL });
|
||||
setupHosts({ isError: true });
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<MCPServerSettings />);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Copy SigNoz instance URL' }),
|
||||
);
|
||||
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('http://localhost');
|
||||
});
|
||||
|
||||
it('shows URL skeleton while hosts are loading', () => {
|
||||
setupGlobalConfig({ mcpUrl: MCP_URL });
|
||||
setupHosts({ isLoading: true });
|
||||
|
||||
render(<MCPServerSettings />);
|
||||
|
||||
expect(screen.queryByTestId('mcp-instance-url')).not.toBeInTheDocument();
|
||||
expect(document.querySelector('.ant-skeleton-input')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not copy while hosts are still loading', async () => {
|
||||
setupGlobalConfig({ mcpUrl: MCP_URL });
|
||||
setupHosts({ isLoading: true });
|
||||
userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<MCPServerSettings />);
|
||||
|
||||
expect(
|
||||
screen.queryByRole('button', { name: 'Copy SigNoz instance URL' }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(mockCopyToClipboard).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disables the hosts query for non-cloud deployments', () => {
|
||||
setupGlobalConfig({ mcpUrl: MCP_URL });
|
||||
setupLicense({ isCloudUser: false, isEnterpriseSelfHostedUser: true });
|
||||
|
||||
render(<MCPServerSettings />, undefined, { role: 'ADMIN' });
|
||||
|
||||
const callOptions = mockUseGetHosts.mock.calls[0]?.[0];
|
||||
expect(callOptions?.query?.enabled).toBe(false);
|
||||
});
|
||||
|
||||
it('uses browser URL immediately for enterprise self-hosted (no skeleton)', async () => {
|
||||
setupGlobalConfig({ mcpUrl: MCP_URL });
|
||||
setupLicense({ isCloudUser: false, isEnterpriseSelfHostedUser: true });
|
||||
setupHosts({ isLoading: false });
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
|
||||
render(<MCPServerSettings />, undefined, { role: 'ADMIN' });
|
||||
|
||||
expect(
|
||||
document.querySelector('.ant-skeleton-input'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('mcp-instance-url')).toHaveTextContent(
|
||||
'http://localhost',
|
||||
);
|
||||
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'Copy SigNoz instance URL' }),
|
||||
);
|
||||
|
||||
expect(mockCopyToClipboard).toHaveBeenCalledWith('http://localhost');
|
||||
});
|
||||
|
||||
it('enables the hosts query for all cloud users including viewers', () => {
|
||||
setupGlobalConfig({ mcpUrl: MCP_URL });
|
||||
setupLicense({ isCloudUser: true });
|
||||
|
||||
render(<MCPServerSettings />, undefined, { role: 'VIEWER' });
|
||||
|
||||
const callOptions = mockUseGetHosts.mock.calls[0]?.[0];
|
||||
expect(callOptions?.query?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it('shows instance URL for cloud viewer', () => {
|
||||
setupGlobalConfig({ mcpUrl: MCP_URL });
|
||||
setupLicense({ isCloudUser: true });
|
||||
setupHosts({
|
||||
hosts: [{ name: 'default', url: DEFAULT_HOST_URL, is_default: true }],
|
||||
});
|
||||
|
||||
render(<MCPServerSettings />, undefined, { role: 'VIEWER' });
|
||||
|
||||
expect(
|
||||
document.querySelector('.ant-skeleton-input'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId('mcp-instance-url')).toHaveTextContent(
|
||||
DEFAULT_HOST_URL,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { SA_QUERY_PARAMS } from 'container/ServiceAccountsSettings/constants';
|
||||
import { useGetGlobalConfig } from 'api/generated/services/global';
|
||||
import { useGetHosts } from 'api/generated/services/zeus';
|
||||
import history from 'lib/history';
|
||||
import { useAppContext } from 'providers/App/App';
|
||||
import { useGetTenantLicense } from 'hooks/useGetTenantLicense';
|
||||
import { USER_ROLES } from 'types/roles';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
|
||||
@@ -36,23 +34,7 @@ function MCPServerSettings(): JSX.Element {
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
const isAdmin = user.role === USER_ROLES.ADMIN;
|
||||
const { isCloudUser } = useGetTenantLicense();
|
||||
|
||||
const {
|
||||
data: hostsData,
|
||||
isLoading: isLoadingHosts,
|
||||
isError: isHostsError,
|
||||
} = useGetHosts({ query: { enabled: isCloudUser } });
|
||||
|
||||
const instanceUrl = useMemo(() => {
|
||||
if (isLoadingHosts || isHostsError || !hostsData) {
|
||||
return getBaseUrl();
|
||||
}
|
||||
const hosts = hostsData.data?.hosts ?? [];
|
||||
const activeHost =
|
||||
hosts.find((h) => !h.is_default) ?? hosts.find((h) => h.is_default);
|
||||
return activeHost?.url ?? getBaseUrl();
|
||||
}, [hostsData, isLoadingHosts, isHostsError]);
|
||||
const instanceUrl = getBaseUrl();
|
||||
|
||||
const { data: globalConfig, isLoading: isConfigLoading } =
|
||||
useGetGlobalConfig();
|
||||
@@ -88,13 +70,10 @@ function MCPServerSettings(): JSX.Element {
|
||||
}, []);
|
||||
|
||||
const handleCopyInstanceUrl = useCallback(() => {
|
||||
if (isLoadingHosts) {
|
||||
return;
|
||||
}
|
||||
copyToClipboard(instanceUrl);
|
||||
toast.success('Instance URL copied to clipboard');
|
||||
void logEvent(ANALYTICS.INSTANCE_URL_COPIED, {});
|
||||
}, [copyToClipboard, instanceUrl, isLoadingHosts]);
|
||||
}, [copyToClipboard, instanceUrl]);
|
||||
|
||||
const handleDocsLinkClick = useCallback((target: string) => {
|
||||
void logEvent(ANALYTICS.DOCS_LINK_CLICKED, { target });
|
||||
@@ -153,7 +132,6 @@ function MCPServerSettings(): JSX.Element {
|
||||
<AuthCard
|
||||
isAdmin={isAdmin}
|
||||
instanceUrl={instanceUrl}
|
||||
isLoadingInstanceUrl={isLoadingHosts}
|
||||
onCopyInstanceUrl={handleCopyInstanceUrl}
|
||||
onCreateServiceAccount={handleCreateServiceAccount}
|
||||
/>
|
||||
|
||||
@@ -89,4 +89,25 @@
|
||||
) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__add-btn {
|
||||
width: 100%;
|
||||
|
||||
// Ensure icon is visible
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l2-foreground) !important;
|
||||
display: inline-block !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--l1-foreground);
|
||||
|
||||
svg,
|
||||
[class*='icon'] {
|
||||
color: var(--l1-foreground) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@ function DomainMappingList({
|
||||
))}
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
variant="dashed"
|
||||
onClick={(): void => add({ domain: '', adminEmail: '' })}
|
||||
prefix={<Plus size={14} />}
|
||||
className="domain-mapping-list__add-btn"
|
||||
>
|
||||
Add Domain Mapping
|
||||
</Button>
|
||||
|
||||
@@ -51,6 +51,35 @@
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
// todo: https://github.com/SigNoz/components/issues/116
|
||||
.roles-search-wrapper {
|
||||
flex: 1;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
background: var(--l3-background);
|
||||
border: 1px solid var(--l1-border);
|
||||
border-radius: 2px;
|
||||
padding: 6px 6px 6px 8px;
|
||||
font-family: Inter;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 18px;
|
||||
letter-spacing: -0.07px;
|
||||
color: var(--l1-foreground);
|
||||
outline: none;
|
||||
height: 32px;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--l3-foreground);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
border-color: var(--input);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.roles-description-tooltip {
|
||||
|
||||
@@ -22,12 +22,14 @@ function RolesSettings(): JSX.Element {
|
||||
</div>
|
||||
<div className="roles-settings-content">
|
||||
<div className="roles-settings-toolbar">
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search for roles..."
|
||||
value={searchQuery}
|
||||
onChange={(e): void => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
<div className="roles-search-wrapper">
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search for roles..."
|
||||
value={searchQuery}
|
||||
onChange={(e): void => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{IS_ROLE_DETAILS_AND_CRUD_ENABLED && (
|
||||
<Button
|
||||
variant="solid"
|
||||
|
||||
@@ -36,7 +36,6 @@ function SettingsPage(): JSX.Element {
|
||||
|
||||
const isAdmin = user.role === USER_ROLES.ADMIN;
|
||||
const isEditor = user.role === USER_ROLES.EDITOR;
|
||||
const isViewer = user.role === USER_ROLES.VIEWER;
|
||||
|
||||
const isWorkspaceBlocked = trialInfo?.workSpaceBlock || false;
|
||||
|
||||
@@ -103,13 +102,6 @@ function SettingsPage(): JSX.Element {
|
||||
: item.isEnabled,
|
||||
}));
|
||||
}
|
||||
|
||||
if (isViewer) {
|
||||
updatedItems = updatedItems.map((item) => ({
|
||||
...item,
|
||||
isEnabled: item.key === ROUTES.MCP_SERVER ? true : item.isEnabled,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (isEnterpriseSelfHostedUser) {
|
||||
@@ -142,13 +134,6 @@ function SettingsPage(): JSX.Element {
|
||||
: item.isEnabled,
|
||||
}));
|
||||
}
|
||||
|
||||
if (isViewer) {
|
||||
updatedItems = updatedItems.map((item) => ({
|
||||
...item,
|
||||
isEnabled: item.key === ROUTES.MCP_SERVER ? true : item.isEnabled,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCloudUser && !isEnterpriseSelfHostedUser) {
|
||||
@@ -181,7 +166,6 @@ function SettingsPage(): JSX.Element {
|
||||
}, [
|
||||
isAdmin,
|
||||
isEditor,
|
||||
isViewer,
|
||||
isCloudUser,
|
||||
isEnterpriseSelfHostedUser,
|
||||
isFetchingActiveLicense,
|
||||
|
||||
@@ -82,13 +82,12 @@ describe('SettingsPage nav sections', () => {
|
||||
expect(screen.getByTestId(id)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each(['billing', 'roles'])('does not render "%s" element', (id) => {
|
||||
expect(screen.queryByTestId(id)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "mcp-server" element', () => {
|
||||
expect(screen.getByTestId('mcp-server')).toBeInTheDocument();
|
||||
});
|
||||
it.each(['billing', 'roles', 'mcp-server'])(
|
||||
'does not render "%s" element',
|
||||
(id) => {
|
||||
expect(screen.queryByTestId(id)).not.toBeInTheDocument();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('Self-hosted Admin', () => {
|
||||
|
||||
@@ -720,10 +720,6 @@ notifications - 2050
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.animate-fast-spin {
|
||||
animation: spin 0.5s linear infinite;
|
||||
}
|
||||
|
||||
// Custom legend tooltip for immediate display
|
||||
.legend-tooltip {
|
||||
position: fixed;
|
||||
|
||||
@@ -5586,10 +5586,10 @@
|
||||
tailwind-merge "^2.5.2"
|
||||
tailwindcss-animate "^1.0.7"
|
||||
|
||||
"@signozhq/ui@0.0.12":
|
||||
version "0.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/ui/-/ui-0.0.12.tgz#b623c1729a0d85532d555fe7e756f3a4207e8e5d"
|
||||
integrity sha512-69XS/j9R+uTNMdupyjki/WK1j0d5K5j0/pJrINGiteQRRrPg/AOMue7v/W6dkLICRhXcz/mgI6tLeT2FAuzKFw==
|
||||
"@signozhq/ui@0.0.10":
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/@signozhq/ui/-/ui-0.0.10.tgz#cdbab838f8cb543cf5b483a86e9d9b65265b81ff"
|
||||
integrity sha512-XLeET+PgSP7heqKMsb9YZOSRT3TpfMPHNQRnY1I4SK8mXSct7BYWwK0Q3Je0uf4Z3aWOcpRYoRUPHWZQBpweFQ==
|
||||
dependencies:
|
||||
"@chenglou/pretext" "^0.0.5"
|
||||
"@radix-ui/react-checkbox" "^1.2.3"
|
||||
@@ -5611,7 +5611,7 @@
|
||||
clsx "^2.1.1"
|
||||
cmdk "^1.1.1"
|
||||
dayjs "^1.11.10"
|
||||
lodash-es "^4.18.1"
|
||||
lodash-es "^4.17.21"
|
||||
motion "^11.11.17"
|
||||
next-themes "^0.4.6"
|
||||
nuqs "^2.8.9"
|
||||
@@ -13291,11 +13291,6 @@ lodash-es@4, lodash-es@^4.17.21:
|
||||
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
|
||||
lodash-es@^4.18.1:
|
||||
version "4.18.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d"
|
||||
integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==
|
||||
|
||||
lodash.camelcase@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz"
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
package signozapiserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/http/handler"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func (provider *provider) addLLMPricingRuleRoutes(router *mux.Router) error {
|
||||
if err := router.Handle("/api/v1/llm_pricing_rules", handler.New(
|
||||
provider.authZ.ViewAccess(provider.llmPricingRuleHandler.List),
|
||||
handler.OpenAPIDef{
|
||||
ID: "ListLLMPricingRules",
|
||||
Tags: []string{"llmpricingrules"},
|
||||
Summary: "List pricing rules",
|
||||
Description: "Returns all LLM pricing rules for the authenticated org, with pagination.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
RequestQuery: new(llmpricingruletypes.ListPricingRulesQuery),
|
||||
Response: new(llmpricingruletypes.GettablePricingRules),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/llm_pricing_rules", handler.New(
|
||||
provider.authZ.AdminAccess(provider.llmPricingRuleHandler.CreateOrUpdate),
|
||||
handler.OpenAPIDef{
|
||||
ID: "CreateOrUpdateLLMPricingRules",
|
||||
Tags: []string{"llmpricingrules"},
|
||||
Summary: "Create or update pricing rules",
|
||||
Description: "Single write endpoint used by both the user and the Zeus sync job. Per-rule match is by id, then sourceId, then insert. Override rows (is_override=true) are fully preserved when the request does not provide isOverride; only synced_at is stamped.",
|
||||
Request: new(llmpricingruletypes.UpdatableLLMPricingRules),
|
||||
RequestContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodPut).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/llm_pricing_rules/{id}", handler.New(
|
||||
provider.authZ.ViewAccess(provider.llmPricingRuleHandler.Get),
|
||||
handler.OpenAPIDef{
|
||||
ID: "GetLLMPricingRule",
|
||||
Tags: []string{"llmpricingrules"},
|
||||
Summary: "Get a pricing rule",
|
||||
Description: "Returns a single LLM pricing rule by ID.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: new(llmpricingruletypes.GettableLLMPricingRule),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/llm_pricing_rules/{id}", handler.New(
|
||||
provider.authZ.AdminAccess(provider.llmPricingRuleHandler.Delete),
|
||||
handler.OpenAPIDef{
|
||||
ID: "DeleteLLMPricingRule",
|
||||
Tags: []string{"llmpricingrules"},
|
||||
Summary: "Delete a pricing rule",
|
||||
Description: "Hard-deletes a pricing rule. If auto-synced, it will be recreated on the next sync cycle.",
|
||||
Request: nil,
|
||||
RequestContentType: "",
|
||||
Response: nil,
|
||||
ResponseContentType: "",
|
||||
SuccessStatusCode: http.StatusNoContent,
|
||||
ErrorStatusCodes: []int{http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
},
|
||||
)).Methods(http.MethodDelete).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
@@ -68,7 +67,6 @@ type provider struct {
|
||||
alertmanagerHandler alertmanager.Handler
|
||||
traceDetailHandler tracedetail.Handler
|
||||
rulerHandler ruler.Handler
|
||||
llmPricingRuleHandler llmpricingrule.Handler
|
||||
}
|
||||
|
||||
func NewFactory(
|
||||
@@ -98,7 +96,6 @@ func NewFactory(
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
spanMapperHandler spanmapper.Handler,
|
||||
alertmanagerHandler alertmanager.Handler,
|
||||
llmPricingRuleHandler llmpricingrule.Handler,
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
|
||||
@@ -133,7 +130,6 @@ func NewFactory(
|
||||
ruleStateHistoryHandler,
|
||||
spanMapperHandler,
|
||||
alertmanagerHandler,
|
||||
llmPricingRuleHandler,
|
||||
traceDetailHandler,
|
||||
rulerHandler,
|
||||
)
|
||||
@@ -170,7 +166,6 @@ func newProvider(
|
||||
ruleStateHistoryHandler rulestatehistory.Handler,
|
||||
spanMapperHandler spanmapper.Handler,
|
||||
alertmanagerHandler alertmanager.Handler,
|
||||
llmPricingRuleHandler llmpricingrule.Handler,
|
||||
traceDetailHandler tracedetail.Handler,
|
||||
rulerHandler ruler.Handler,
|
||||
) (apiserver.APIServer, error) {
|
||||
@@ -207,7 +202,6 @@ func newProvider(
|
||||
alertmanagerHandler: alertmanagerHandler,
|
||||
traceDetailHandler: traceDetailHandler,
|
||||
rulerHandler: rulerHandler,
|
||||
llmPricingRuleHandler: llmPricingRuleHandler,
|
||||
}
|
||||
|
||||
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
|
||||
@@ -320,10 +314,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addLLMPricingRuleRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := provider.addTraceDetailRoutes(router); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -451,22 +451,6 @@ func (provider *provider) addQuerierRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v5/query_range/preview", handler.New(provider.authZ.ViewAccess(provider.querierHandler.QueryRangePreview), handler.OpenAPIDef{
|
||||
ID: "QueryRangePreviewV5",
|
||||
Tags: []string{"querier"},
|
||||
Summary: "Query range (dry-run)",
|
||||
Description: "Validates the request and renders the underlying SQL/PromQL for each query in the composite query without executing it. When the 'explain' query parameter is set to true, an EXPLAIN is run against ClickHouse for each rendered SQL statement.",
|
||||
Request: new(qbtypes.QueryRangeRequest),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(qbtypes.QueryRangePreviewResponse),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest},
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
})).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v5/substitute_vars", handler.New(provider.authZ.ViewAccess(provider.querierHandler.ReplaceVariables), handler.OpenAPIDef{
|
||||
ID: "ReplaceVariables",
|
||||
Tags: []string{"querier"},
|
||||
|
||||
@@ -27,7 +27,7 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authZ.ViewAccess(provider.zeusHandler.GetHosts), handler.OpenAPIDef{
|
||||
if err := router.Handle("/api/v2/zeus/hosts", handler.New(provider.authZ.AdminAccess(provider.zeusHandler.GetHosts), handler.OpenAPIDef{
|
||||
ID: "GetHosts",
|
||||
Tags: []string{"zeus"},
|
||||
Summary: "Get host info from Zeus.",
|
||||
@@ -39,7 +39,7 @@ func (provider *provider) addZeusRoutes(router *mux.Router) error {
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
|
||||
})).Methods(http.MethodGet).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
package impllmpricingrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/factory"
|
||||
"github.com/SigNoz/signoz/pkg/http/binding"
|
||||
"github.com/SigNoz/signoz/pkg/http/render"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/types/authtypes"
|
||||
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
const maxLimit = 100
|
||||
|
||||
type handler struct {
|
||||
module llmpricingrule.Module
|
||||
providerSettings factory.ProviderSettings
|
||||
}
|
||||
|
||||
func NewHandler(module llmpricingrule.Module, providerSettings factory.ProviderSettings) llmpricingrule.Handler {
|
||||
return &handler{module: module, providerSettings: providerSettings}
|
||||
}
|
||||
|
||||
// List handles GET /api/v1/llm_pricing_rules.
|
||||
func (h *handler) List(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
var q llmpricingruletypes.ListPricingRulesQuery
|
||||
if err := binding.Query.BindQuery(r.URL.Query(), &q); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if q.Limit <= 0 {
|
||||
q.Limit = 20
|
||||
} else if q.Limit > maxLimit {
|
||||
q.Limit = maxLimit
|
||||
}
|
||||
if q.Offset < 0 {
|
||||
render.Error(rw, errors.Newf(errors.TypeInvalidInput, llmpricingruletypes.ErrCodePricingRuleInvalidInput, "offset must be a non-negative integer"))
|
||||
return
|
||||
}
|
||||
|
||||
rules, total, err := h.module.List(ctx, orgID, q.Offset, q.Limit)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, llmpricingruletypes.NewGettableLLMPricingRulesFromLLMPricingRules(rules, total, q.Offset, q.Limit))
|
||||
}
|
||||
|
||||
// Get handles GET /api/v1/llm_pricing_rules/{id}.
|
||||
func (h *handler) Get(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
id, err := ruleIDFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
rule, err := h.module.Get(ctx, orgID, id)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, rule)
|
||||
}
|
||||
|
||||
func (h *handler) CreateOrUpdate(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
req := new(llmpricingruletypes.UpdatableLLMPricingRules)
|
||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = h.module.CreateOrUpdate(ctx, orgID, claims.Email, req.Rules)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// Delete handles DELETE /api/v1/llm_pricing_rules/{id}.
|
||||
func (h *handler) Delete(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
id, err := ruleIDFromPath(r)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.module.Delete(ctx, orgID, id); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// ruleIDFromPath extracts and validates the {id} path variable.
|
||||
func ruleIDFromPath(r *http.Request) (valuer.UUID, error) {
|
||||
raw := mux.Vars(r)["id"]
|
||||
id, err := valuer.NewUUID(raw)
|
||||
if err != nil {
|
||||
return valuer.UUID{}, errors.Wrapf(err, errors.TypeInvalidInput, llmpricingruletypes.ErrCodePricingRuleInvalidInput, "id is not a valid uuid")
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package llmpricingrule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/types/llmpricingruletypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Module interface {
|
||||
List(ctx context.Context, orgID valuer.UUID, offset, limit int) ([]*llmpricingruletypes.LLMPricingRule, int, error)
|
||||
Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*llmpricingruletypes.LLMPricingRule, error)
|
||||
CreateOrUpdate(ctx context.Context, orgID valuer.UUID, userEmail string, rules []llmpricingruletypes.UpdatableLLMPricingRule) (err error)
|
||||
Delete(ctx context.Context, orgID, id valuer.UUID) error
|
||||
}
|
||||
|
||||
// Handler defines the HTTP handler interface for pricing rule endpoints.
|
||||
type Handler interface {
|
||||
List(rw http.ResponseWriter, r *http.Request)
|
||||
Get(rw http.ResponseWriter, r *http.Request)
|
||||
CreateOrUpdate(rw http.ResponseWriter, r *http.Request)
|
||||
Delete(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
@@ -72,53 +72,6 @@ func (handler *handler) QueryRange(rw http.ResponseWriter, req *http.Request) {
|
||||
|
||||
render.Success(rw, http.StatusOK, queryRangeResponse)
|
||||
}
|
||||
|
||||
func (handler *handler) QueryRangePreview(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
instrumentationtypes.CodeNamespace: "querier",
|
||||
instrumentationtypes.CodeFunctionName: "QueryRangePreview",
|
||||
})
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
var queryRangeRequest qbtypes.QueryRangeRequest
|
||||
if err := json.NewDecoder(req.Body).Decode(&queryRangeRequest); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := queryRangeRequest.Validate(); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := valuer.NewUUID(claims.OrgID)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
explain, err := ParseExplainVariant(req.URL.Query().Get("explain"))
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
opts := qbtypes.QueryRangePreviewOptions{Explain: explain}
|
||||
previewResp, err := handler.querier.QueryRangePreview(ctx, orgID, &queryRangeRequest, opts)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, previewResp)
|
||||
}
|
||||
|
||||
func (handler *handler) QueryRawStream(rw http.ResponseWriter, req *http.Request) {
|
||||
ctx := req.Context()
|
||||
|
||||
|
||||
@@ -192,12 +192,6 @@ func (q *builderQuery[T]) isWindowList() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Statement renders the SQL statement for the builder query without executing
|
||||
// it. It is used by the dry-run/preview path.
|
||||
func (q *builderQuery[T]) Statement(ctx context.Context) (*qbtypes.Statement, error) {
|
||||
return q.stmtBuilder.Build(ctx, q.fromMS, q.toMS, q.kind, q.spec, q.variables)
|
||||
}
|
||||
|
||||
func (q *builderQuery[T]) Execute(ctx context.Context) (*qbtypes.Result, error) {
|
||||
|
||||
// can we do window based pagination?
|
||||
|
||||
@@ -99,16 +99,6 @@ func (q *chSQLQuery) renderVars(query string, vars map[string]qbtypes.VariableIt
|
||||
return newQuery.String(), nil
|
||||
}
|
||||
|
||||
// Statement renders the SQL statement for the ClickHouse SQL query without
|
||||
// executing it. It is used by the dry-run/preview path.
|
||||
func (q *chSQLQuery) Statement(_ context.Context) (*qbtypes.Statement, error) {
|
||||
rendered, err := q.renderVars(q.query.Query, q.vars, q.fromMS, q.toMS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &qbtypes.Statement{Query: rendered, Args: q.args}, nil
|
||||
}
|
||||
|
||||
func (q *chSQLQuery) Execute(ctx context.Context) (*qbtypes.Result, error) {
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
instrumentationtypes.QueryDuration: instrumentationtypes.DurationBucket(q.fromMS, q.toMS),
|
||||
|
||||
@@ -12,11 +12,6 @@ import (
|
||||
type Querier interface {
|
||||
QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest) (*qbtypes.QueryRangeResponse, error)
|
||||
QueryRawStream(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest, client *qbtypes.RawStream)
|
||||
// QueryRangePreview validates and renders the queries in req without
|
||||
// executing them. opts controls dry-run behavior such as which
|
||||
// EXPLAIN variant to attach to the response; the zero value performs
|
||||
// a validation-only preview with no EXPLAIN.
|
||||
QueryRangePreview(ctx context.Context, orgID valuer.UUID, req *qbtypes.QueryRangeRequest, opts qbtypes.QueryRangePreviewOptions) (*qbtypes.QueryRangePreviewResponse, error)
|
||||
}
|
||||
|
||||
// BucketCache is the interface for bucket-based caching.
|
||||
@@ -30,6 +25,5 @@ type BucketCache interface {
|
||||
type Handler interface {
|
||||
QueryRange(rw http.ResponseWriter, req *http.Request)
|
||||
QueryRawStream(rw http.ResponseWriter, req *http.Request)
|
||||
QueryRangePreview(rw http.ResponseWriter, req *http.Request)
|
||||
ReplaceVariables(rw http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
|
||||
@@ -627,19 +627,29 @@ func convertTimeSeriesDataToScalar(tsData *qbtypes.TimeSeriesData, queryName str
|
||||
return &qbtypes.ScalarData{QueryName: queryName}
|
||||
}
|
||||
|
||||
columns := []*qbtypes.ColumnDescriptor{}
|
||||
|
||||
// Add group columns from first series
|
||||
if len(tsData.Aggregations[0].Series) > 0 {
|
||||
for _, label := range tsData.Aggregations[0].Series[0].Labels {
|
||||
columns = append(columns, &qbtypes.ColumnDescriptor{
|
||||
TelemetryFieldKey: label.Key,
|
||||
QueryName: queryName,
|
||||
Type: qbtypes.ColumnTypeGroup,
|
||||
})
|
||||
// Series can have ragged label sets; build the column schema from the
|
||||
// union of all label keys (first-seen order) and fill rows by key lookup.
|
||||
keyOrder := []telemetrytypes.TelemetryFieldKey{}
|
||||
keyIndex := map[string]int{}
|
||||
for _, series := range tsData.Aggregations[0].Series {
|
||||
for _, label := range series.Labels {
|
||||
if _, ok := keyIndex[label.Key.Name]; ok {
|
||||
continue
|
||||
}
|
||||
keyIndex[label.Key.Name] = len(keyOrder)
|
||||
keyOrder = append(keyOrder, label.Key)
|
||||
}
|
||||
}
|
||||
|
||||
columns := make([]*qbtypes.ColumnDescriptor, 0, len(keyOrder)+len(tsData.Aggregations))
|
||||
for _, key := range keyOrder {
|
||||
columns = append(columns, &qbtypes.ColumnDescriptor{
|
||||
TelemetryFieldKey: key,
|
||||
QueryName: queryName,
|
||||
Type: qbtypes.ColumnTypeGroup,
|
||||
})
|
||||
}
|
||||
|
||||
// Add aggregation columns
|
||||
for _, agg := range tsData.Aggregations {
|
||||
name := agg.Alias
|
||||
@@ -655,18 +665,18 @@ func convertTimeSeriesDataToScalar(tsData *qbtypes.TimeSeriesData, queryName str
|
||||
})
|
||||
}
|
||||
|
||||
// Build rows
|
||||
// Build rows.
|
||||
groupColCount := len(keyOrder)
|
||||
data := [][]any{}
|
||||
for seriesIdx, series := range tsData.Aggregations[0].Series {
|
||||
row := make([]any, len(columns))
|
||||
|
||||
// Add group values
|
||||
for i, label := range series.Labels {
|
||||
row[i] = label.Value
|
||||
// Place each label under its key's column (by lookup, not index).
|
||||
for _, label := range series.Labels {
|
||||
row[keyIndex[label.Key.Name]] = label.Value
|
||||
}
|
||||
|
||||
// Add aggregation values (last value)
|
||||
groupColCount := len(series.Labels)
|
||||
for aggIdx, agg := range tsData.Aggregations {
|
||||
if seriesIdx < len(agg.Series) && len(agg.Series[seriesIdx].Values) > 0 {
|
||||
lastValue := agg.Series[seriesIdx].Values[len(agg.Series[seriesIdx].Values)-1].Value
|
||||
|
||||
53
pkg/querier/postprocess_test.go
Normal file
53
pkg/querier/postprocess_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package querier
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
)
|
||||
|
||||
// Multiple series with different number of labels, shouldn't panic and should align labels correctly.
|
||||
func TestConvertTimeSeriesDataToScalar_RaggedLabels(t *testing.T) {
|
||||
label := func(name string, value any) *qbtypes.Label {
|
||||
return &qbtypes.Label{
|
||||
Key: telemetrytypes.TelemetryFieldKey{Name: name},
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
series := func(labels []*qbtypes.Label, value float64) *qbtypes.TimeSeries {
|
||||
return &qbtypes.TimeSeries{
|
||||
Labels: labels,
|
||||
Values: []*qbtypes.TimeSeriesValue{{Timestamp: 1, Value: value}},
|
||||
}
|
||||
}
|
||||
|
||||
tsData := &qbtypes.TimeSeriesData{
|
||||
QueryName: "A",
|
||||
Aggregations: []*qbtypes.AggregationBucket{{
|
||||
Index: 0,
|
||||
Series: []*qbtypes.TimeSeries{
|
||||
series([]*qbtypes.Label{label("label_1", "orphan-0")}, 20),
|
||||
series([]*qbtypes.Label{label("label_1", "box-0"), label("label_2", "rpc-0")}, 10),
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
var sd *qbtypes.ScalarData
|
||||
require.NotPanics(t, func() {
|
||||
sd = convertTimeSeriesDataToScalar(tsData, "A")
|
||||
})
|
||||
|
||||
require.NotNil(t, sd)
|
||||
require.Len(t, sd.Columns, 3)
|
||||
assert.Equal(t, "label_1", sd.Columns[0].Name)
|
||||
assert.Equal(t, "label_2", sd.Columns[1].Name)
|
||||
assert.Equal(t, "__result_0", sd.Columns[2].Name)
|
||||
|
||||
require.Len(t, sd.Data, 2)
|
||||
assert.Equal(t, []any{"orphan-0", nil, 20.0}, sd.Data[0])
|
||||
assert.Equal(t, []any{"box-0", "rpc-0", 10.0}, sd.Data[1])
|
||||
}
|
||||
@@ -1,360 +0,0 @@
|
||||
package querier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/querybuilder"
|
||||
"github.com/SigNoz/signoz/pkg/types/metrictypes"
|
||||
qbtypes "github.com/SigNoz/signoz/pkg/types/querybuildertypes/querybuildertypesv5"
|
||||
"github.com/SigNoz/signoz/pkg/types/telemetrytypes"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
// statementProvider is implemented by query types that can render the
|
||||
// underlying SQL/PromQL statement without executing it.
|
||||
type statementProvider interface {
|
||||
Statement(ctx context.Context) (*qbtypes.Statement, error)
|
||||
}
|
||||
|
||||
// clickhouseExplainClause maps a variant to the EXPLAIN clause understood
|
||||
// by ClickHouse (i.e. what comes between EXPLAIN and the SELECT).
|
||||
func clickhouseExplainClause(v qbtypes.ExplainVariant) (string, bool) {
|
||||
switch v {
|
||||
case qbtypes.ExplainVariantPlan:
|
||||
return "PLAN", true
|
||||
case qbtypes.ExplainVariantAST:
|
||||
return "AST", true
|
||||
case qbtypes.ExplainVariantSyntax:
|
||||
return "SYNTAX", true
|
||||
case qbtypes.ExplainVariantPipeline:
|
||||
return "PIPELINE", true
|
||||
case qbtypes.ExplainVariantEstimate:
|
||||
return "ESTIMATE", true
|
||||
case qbtypes.ExplainVariantQueryTree:
|
||||
return "QUERY TREE", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// ParseExplainVariant parses the ?explain= query parameter. An empty value
|
||||
// (or "false") returns ExplainVariantNone. The literal "true" maps to PLAN
|
||||
// for back-compat with simple ?explain=true. Otherwise the value must
|
||||
// match one of the named variants.
|
||||
func ParseExplainVariant(value string) (qbtypes.ExplainVariant, error) {
|
||||
token := strings.ToLower(strings.TrimSpace(value))
|
||||
switch token {
|
||||
case "", "false":
|
||||
return qbtypes.ExplainVariantNone, nil
|
||||
case "true":
|
||||
return qbtypes.ExplainVariantPlan, nil
|
||||
}
|
||||
v := qbtypes.ExplainVariant(token)
|
||||
if _, ok := clickhouseExplainClause(v); !ok {
|
||||
return qbtypes.ExplainVariantNone, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported explain variant %q (allowed: plan, ast, syntax, pipeline, estimate, query_tree)", token)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// QueryRangePreview validates the request and renders the underlying SQL
|
||||
// (or PromQL) for each query in the composite query without executing it.
|
||||
// When opts.Explain is non-empty, EXPLAIN <variant> is run against the
|
||||
// telemetry store for each rendered SQL statement and attached to the
|
||||
// response.
|
||||
func (q *querier) QueryRangePreview(
|
||||
ctx context.Context,
|
||||
_ valuer.UUID,
|
||||
req *qbtypes.QueryRangeRequest,
|
||||
opts qbtypes.QueryRangePreviewOptions,
|
||||
) (*qbtypes.QueryRangePreviewResponse, error) {
|
||||
tmplVars := req.Variables
|
||||
if tmplVars == nil {
|
||||
tmplVars = make(map[string]qbtypes.VariableItem)
|
||||
}
|
||||
|
||||
dependencyQueries := make(map[string]bool)
|
||||
for _, query := range req.CompositeQuery.Queries {
|
||||
if query.Type == qbtypes.QueryTypeTraceOperator {
|
||||
if spec, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator); ok {
|
||||
if err := spec.ParseExpression(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, dep := range spec.CollectReferencedQueries(spec.ParsedExpression) {
|
||||
dependencyQueries[dep] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First pass: normalize step intervals and collect metric names that
|
||||
// need temporality/type lookup.
|
||||
metricNames := make([]string, 0)
|
||||
for idx, query := range req.CompositeQuery.Queries {
|
||||
switch query.Type {
|
||||
case qbtypes.QueryTypeBuilder:
|
||||
if spec, ok := query.Spec.(qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]); ok {
|
||||
for _, agg := range spec.Aggregations {
|
||||
if agg.MetricName != "" {
|
||||
metricNames = append(metricNames, agg.MetricName)
|
||||
}
|
||||
}
|
||||
}
|
||||
switch spec := query.Spec.(type) {
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
||||
if spec.StepInterval.Seconds() == 0 {
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.RecommendedStepInterval(req.Start, req.End))}
|
||||
}
|
||||
if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepInterval(req.Start, req.End)) {
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.MinAllowedStepInterval(req.Start, req.End))}
|
||||
}
|
||||
req.CompositeQuery.Queries[idx].Spec = spec
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
|
||||
if spec.StepInterval.Seconds() == 0 {
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.RecommendedStepInterval(req.Start, req.End))}
|
||||
}
|
||||
if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepInterval(req.Start, req.End)) {
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.MinAllowedStepInterval(req.Start, req.End))}
|
||||
}
|
||||
req.CompositeQuery.Queries[idx].Spec = spec
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
|
||||
if spec.Source == telemetrytypes.SourceMeter {
|
||||
if spec.StepInterval.Seconds() == 0 {
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.RecommendedStepIntervalForMeter(req.Start, req.End))}
|
||||
}
|
||||
if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepIntervalForMeter(req.Start, req.End)) {
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.MinAllowedStepIntervalForMeter(req.Start, req.End))}
|
||||
}
|
||||
} else {
|
||||
if spec.StepInterval.Seconds() == 0 {
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.RecommendedStepIntervalForMetric(req.Start, req.End))}
|
||||
}
|
||||
if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepIntervalForMetric(req.Start, req.End)) {
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.MinAllowedStepIntervalForMetric(req.Start, req.End))}
|
||||
}
|
||||
}
|
||||
req.CompositeQuery.Queries[idx].Spec = spec
|
||||
}
|
||||
case qbtypes.QueryTypePromQL:
|
||||
if spec, ok := query.Spec.(qbtypes.PromQuery); ok {
|
||||
if spec.Step.Seconds() == 0 {
|
||||
spec.Step = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.RecommendedStepIntervalForMetric(req.Start, req.End))}
|
||||
}
|
||||
req.CompositeQuery.Queries[idx].Spec = spec
|
||||
}
|
||||
case qbtypes.QueryTypeTraceOperator:
|
||||
if spec, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator); ok {
|
||||
if spec.StepInterval.Seconds() == 0 {
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.RecommendedStepInterval(req.Start, req.End))}
|
||||
}
|
||||
if spec.StepInterval.Seconds() < float64(querybuilder.MinAllowedStepInterval(req.Start, req.End)) {
|
||||
spec.StepInterval = qbtypes.Step{Duration: time.Second * time.Duration(querybuilder.MinAllowedStepInterval(req.Start, req.End))}
|
||||
}
|
||||
req.CompositeQuery.Queries[idx].Spec = spec
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch metric temporality/type for any builder metric queries.
|
||||
var metricTemporality map[string]metrictypes.Temporality
|
||||
var metricTypes map[string]metrictypes.Type
|
||||
if len(metricNames) > 0 {
|
||||
var err error
|
||||
metricTemporality, metricTypes, err = q.metadataStore.FetchTemporalityAndTypeMulti(ctx, req.Start, req.End, metricNames...)
|
||||
if err != nil {
|
||||
q.logger.WarnContext(ctx, "failed to fetch metric temporality during preview", errors.Attr(err), slog.Any("metrics", metricNames))
|
||||
return nil, errors.NewInternalf(errors.CodeInternal, "failed to fetch metrics temporality")
|
||||
}
|
||||
}
|
||||
|
||||
type queryEntry struct {
|
||||
name string
|
||||
queryType qbtypes.QueryType
|
||||
query qbtypes.Query
|
||||
}
|
||||
entries := make([]queryEntry, 0, len(req.CompositeQuery.Queries))
|
||||
|
||||
for _, query := range req.CompositeQuery.Queries {
|
||||
var queryName string
|
||||
isTraceOperator := query.Type == qbtypes.QueryTypeTraceOperator
|
||||
|
||||
switch query.Type {
|
||||
case qbtypes.QueryTypeTraceOperator:
|
||||
if spec, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator); ok {
|
||||
queryName = spec.Name
|
||||
}
|
||||
case qbtypes.QueryTypePromQL:
|
||||
if spec, ok := query.Spec.(qbtypes.PromQuery); ok {
|
||||
queryName = spec.Name
|
||||
}
|
||||
case qbtypes.QueryTypeClickHouseSQL:
|
||||
if spec, ok := query.Spec.(qbtypes.ClickHouseQuery); ok {
|
||||
queryName = spec.Name
|
||||
}
|
||||
case qbtypes.QueryTypeBuilder:
|
||||
switch spec := query.Spec.(type) {
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
||||
queryName = spec.Name
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
|
||||
queryName = spec.Name
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
|
||||
queryName = spec.Name
|
||||
}
|
||||
}
|
||||
|
||||
if !isTraceOperator && dependencyQueries[queryName] {
|
||||
continue
|
||||
}
|
||||
|
||||
switch query.Type {
|
||||
case qbtypes.QueryTypePromQL:
|
||||
promQuery, ok := query.Spec.(qbtypes.PromQuery)
|
||||
if !ok {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid promql query spec %T", query.Spec)
|
||||
}
|
||||
pq := newPromqlQuery(q.logger, q.promEngine, promQuery, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType, tmplVars)
|
||||
entries = append(entries, queryEntry{name: promQuery.Name, queryType: query.Type, query: pq})
|
||||
case qbtypes.QueryTypeClickHouseSQL:
|
||||
chQuery, ok := query.Spec.(qbtypes.ClickHouseQuery)
|
||||
if !ok {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid clickhouse query spec %T", query.Spec)
|
||||
}
|
||||
cq := newchSQLQuery(q.logger, q.telemetryStore, chQuery, nil, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType, tmplVars)
|
||||
entries = append(entries, queryEntry{name: chQuery.Name, queryType: query.Type, query: cq})
|
||||
case qbtypes.QueryTypeTraceOperator:
|
||||
traceOpQuery, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator)
|
||||
if !ok {
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid trace operator query spec %T", query.Spec)
|
||||
}
|
||||
toq := &traceOperatorQuery{
|
||||
telemetryStore: q.telemetryStore,
|
||||
stmtBuilder: q.traceOperatorStmtBuilder,
|
||||
spec: traceOpQuery,
|
||||
compositeQuery: &req.CompositeQuery,
|
||||
fromMS: uint64(req.Start),
|
||||
toMS: uint64(req.End),
|
||||
kind: req.RequestType,
|
||||
}
|
||||
entries = append(entries, queryEntry{name: traceOpQuery.Name, queryType: query.Type, query: toq})
|
||||
case qbtypes.QueryTypeBuilder:
|
||||
switch spec := query.Spec.(type) {
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.TraceAggregation]:
|
||||
spec.ShiftBy = extractShiftFromBuilderQuery(spec)
|
||||
timeRange := adjustTimeRangeForShift(spec, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType)
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, q.traceStmtBuilder, spec, timeRange, req.RequestType, tmplVars)
|
||||
entries = append(entries, queryEntry{name: spec.Name, queryType: query.Type, query: bq})
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.LogAggregation]:
|
||||
spec.ShiftBy = extractShiftFromBuilderQuery(spec)
|
||||
timeRange := adjustTimeRangeForShift(spec, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType)
|
||||
stmtBuilder := q.logStmtBuilder
|
||||
if spec.Source == telemetrytypes.SourceAudit {
|
||||
stmtBuilder = q.auditStmtBuilder
|
||||
}
|
||||
bq := newBuilderQuery(q.logger, q.telemetryStore, stmtBuilder, spec, timeRange, req.RequestType, tmplVars)
|
||||
entries = append(entries, queryEntry{name: spec.Name, queryType: query.Type, query: bq})
|
||||
case qbtypes.QueryBuilderQuery[qbtypes.MetricAggregation]:
|
||||
presentAggregations := []qbtypes.MetricAggregation{}
|
||||
for i := range spec.Aggregations {
|
||||
if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Temporality == metrictypes.Unknown {
|
||||
if temp, ok := metricTemporality[spec.Aggregations[i].MetricName]; ok && temp != metrictypes.Unknown {
|
||||
spec.Aggregations[i].Temporality = temp
|
||||
}
|
||||
}
|
||||
if spec.Aggregations[i].MetricName != "" && spec.Aggregations[i].Type == metrictypes.UnspecifiedType {
|
||||
if foundMetricType, ok := metricTypes[spec.Aggregations[i].MetricName]; ok && foundMetricType != metrictypes.UnspecifiedType {
|
||||
spec.Aggregations[i].Type = foundMetricType
|
||||
}
|
||||
}
|
||||
if spec.Aggregations[i].Type == metrictypes.UnspecifiedType {
|
||||
continue
|
||||
}
|
||||
presentAggregations = append(presentAggregations, spec.Aggregations[i])
|
||||
}
|
||||
if len(presentAggregations) == 0 {
|
||||
// nothing renderable for this query — skip
|
||||
continue
|
||||
}
|
||||
spec.Aggregations = presentAggregations
|
||||
spec.ShiftBy = extractShiftFromBuilderQuery(spec)
|
||||
timeRange := adjustTimeRangeForShift(spec, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType)
|
||||
var bq *builderQuery[qbtypes.MetricAggregation]
|
||||
if spec.Source == telemetrytypes.SourceMeter {
|
||||
bq = newBuilderQuery(q.logger, q.telemetryStore, q.meterStmtBuilder, spec, timeRange, req.RequestType, tmplVars)
|
||||
} else {
|
||||
bq = newBuilderQuery(q.logger, q.telemetryStore, q.metricStmtBuilder, spec, timeRange, req.RequestType, tmplVars)
|
||||
}
|
||||
entries = append(entries, queryEntry{name: spec.Name, queryType: query.Type, query: bq})
|
||||
default:
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported builder spec type %T", query.Spec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statements := make([]*qbtypes.PreviewStatement, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
provider, ok := entry.query.(statementProvider)
|
||||
if !ok {
|
||||
return nil, errors.NewInternalf(errors.CodeInternal, "query %s does not support preview", entry.name)
|
||||
}
|
||||
stmt, err := provider.Statement(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ps := &qbtypes.PreviewStatement{
|
||||
QueryName: entry.name,
|
||||
QueryType: entry.queryType.StringValue(),
|
||||
Query: stmt.Query,
|
||||
Args: stmt.Args,
|
||||
Warnings: stmt.Warnings,
|
||||
}
|
||||
|
||||
if opts.Explain != qbtypes.ExplainVariantNone && entry.queryType != qbtypes.QueryTypePromQL {
|
||||
out, err := q.runExplain(ctx, opts.Explain, stmt.Query, stmt.Args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ps.ExplainVariant = string(opts.Explain)
|
||||
ps.Explain = out
|
||||
}
|
||||
|
||||
statements = append(statements, ps)
|
||||
}
|
||||
|
||||
return &qbtypes.QueryRangePreviewResponse{
|
||||
Type: req.RequestType,
|
||||
Statements: statements,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// runExplain runs `EXPLAIN <variant> <stmt>` against the telemetry store
|
||||
// and returns the formatted output as a single string with one row per
|
||||
// line.
|
||||
func (q *querier) runExplain(ctx context.Context, variant qbtypes.ExplainVariant, stmt string, args []any) (string, error) {
|
||||
clause, ok := clickhouseExplainClause(variant)
|
||||
if !ok {
|
||||
return "", errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported explain variant %q", string(variant))
|
||||
}
|
||||
explainQuery := "EXPLAIN " + clause + " " + stmt
|
||||
rows, err := q.telemetryStore.ClickhouseDB().Query(ctx, explainQuery, args...)
|
||||
if err != nil {
|
||||
return "", errors.WrapInternalf(err, errors.CodeInternal, "failed to run EXPLAIN %s", clause)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var lines []string
|
||||
for rows.Next() {
|
||||
var line string
|
||||
if err := rows.Scan(&line); err != nil {
|
||||
return "", errors.WrapInternalf(err, errors.CodeInternal, "failed to scan EXPLAIN row")
|
||||
}
|
||||
lines = append(lines, line)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return "", errors.WrapInternalf(err, errors.CodeInternal, "EXPLAIN row iteration failed")
|
||||
}
|
||||
return strings.Join(lines, "\n"), nil
|
||||
}
|
||||
@@ -211,17 +211,6 @@ func (q *promqlQuery) renderVars(query string, vars map[string]qbv5.VariableItem
|
||||
return newQuery.String(), nil
|
||||
}
|
||||
|
||||
// Statement renders the PromQL query string after variable substitution. It
|
||||
// is used by the dry-run/preview path; PromQL queries do not have a
|
||||
// SQL-style argument list.
|
||||
func (q *promqlQuery) Statement(_ context.Context) (*qbv5.Statement, error) {
|
||||
rendered, err := q.renderVars(q.query.Query, q.vars, q.tr.From, q.tr.To)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &qbv5.Statement{Query: rendered}, nil
|
||||
}
|
||||
|
||||
func (q *promqlQuery) Execute(ctx context.Context) (*qbv5.Result, error) {
|
||||
|
||||
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
|
||||
|
||||
@@ -32,12 +32,6 @@ func (q *traceOperatorQuery) Window() (uint64, uint64) {
|
||||
return q.fromMS, q.toMS
|
||||
}
|
||||
|
||||
// Statement renders the SQL statement for the trace operator query without
|
||||
// executing it. It is used by the dry-run/preview path.
|
||||
func (q *traceOperatorQuery) Statement(ctx context.Context) (*qbtypes.Statement, error) {
|
||||
return q.stmtBuilder.Build(ctx, q.fromMS, q.toMS, q.kind, q.spec, q.compositeQuery)
|
||||
}
|
||||
|
||||
func (q *traceOperatorQuery) Execute(ctx context.Context) (*qbtypes.Result, error) {
|
||||
stmt, err := q.stmtBuilder.Build(
|
||||
ctx,
|
||||
|
||||
@@ -22,8 +22,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields/implfields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring/implinframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule/impllmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer/implmetricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/quickfilter"
|
||||
@@ -79,7 +77,6 @@ type Handlers struct {
|
||||
AlertmanagerHandler alertmanager.Handler
|
||||
TraceDetail tracedetail.Handler
|
||||
RulerHandler ruler.Handler
|
||||
LLMPricingRuleHandler llmpricingrule.Handler
|
||||
}
|
||||
|
||||
func NewHandlers(
|
||||
@@ -124,6 +121,5 @@ func NewHandlers(
|
||||
AlertmanagerHandler: signozalertmanager.NewHandler(alertmanagerService),
|
||||
TraceDetail: impltracedetail.NewHandler(modules.TraceDetail),
|
||||
RulerHandler: signozruler.NewHandler(rulerService),
|
||||
LLMPricingRuleHandler: impllmpricingrule.NewHandler(nil, providerSettings),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/SigNoz/signoz/pkg/modules/dashboard"
|
||||
"github.com/SigNoz/signoz/pkg/modules/fields"
|
||||
"github.com/SigNoz/signoz/pkg/modules/inframonitoring"
|
||||
"github.com/SigNoz/signoz/pkg/modules/llmpricingrule"
|
||||
"github.com/SigNoz/signoz/pkg/modules/metricsexplorer"
|
||||
"github.com/SigNoz/signoz/pkg/modules/organization"
|
||||
"github.com/SigNoz/signoz/pkg/modules/preference"
|
||||
@@ -78,7 +77,6 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
|
||||
struct{ rulestatehistory.Handler }{},
|
||||
struct{ spanmapper.Handler }{},
|
||||
struct{ alertmanager.Handler }{},
|
||||
struct{ llmpricingrule.Handler }{},
|
||||
struct{ tracedetail.Handler }{},
|
||||
struct{ ruler.Handler }{},
|
||||
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
|
||||
|
||||
@@ -283,7 +283,6 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
|
||||
handlers.RuleStateHistory,
|
||||
handlers.SpanMapperHandler,
|
||||
handlers.AlertmanagerHandler,
|
||||
handlers.LLMPricingRuleHandler,
|
||||
handlers.TraceDetail,
|
||||
handlers.RulerHandler,
|
||||
),
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
package llmpricingruletypes
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/types"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodePricingRuleNotFound = errors.MustNewCode("pricing_rule_not_found")
|
||||
ErrCodePricingRuleInvalidInput = errors.MustNewCode("pricing_rule_invalid_input")
|
||||
)
|
||||
|
||||
type LLMPricingRuleUnit struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
UnitPerMillionTokens = LLMPricingRuleUnit{valuer.NewString("per_million_tokens")}
|
||||
)
|
||||
|
||||
type LLMPricingRuleCacheMode struct {
|
||||
valuer.String
|
||||
}
|
||||
|
||||
var (
|
||||
// LLMPricingRuleCacheModeSubtract: cached tokens are inside input_tokens (OpenAI-style).
|
||||
LLMPricingRuleCacheModeSubtract = LLMPricingRuleCacheMode{valuer.NewString("subtract")}
|
||||
// LLMPricingRuleCacheModeAdditive: cached tokens are reported separately (Anthropic-style).
|
||||
LLMPricingRuleCacheModeAdditive = LLMPricingRuleCacheMode{valuer.NewString("additive")}
|
||||
// LLMPricingRuleCacheModeUnknown: provider behaviour is unknown; falls back to subtract.
|
||||
LLMPricingRuleCacheModeUnknown = LLMPricingRuleCacheMode{valuer.NewString("unknown")}
|
||||
)
|
||||
|
||||
// StringSlice is a []string that is stored as a JSON text column.
|
||||
// It is compatible with both SQLite and PostgreSQL.
|
||||
type StringSlice []string
|
||||
|
||||
// LLMRulePricing is the per-rule pricing shape, persisted as a single JSON.
|
||||
type LLMRulePricing struct {
|
||||
Input float64 `json:"input" required:"true"`
|
||||
Output float64 `json:"output" required:"true"`
|
||||
Cache *LLMPricingCacheCosts `json:"cache,omitempty"`
|
||||
}
|
||||
|
||||
type LLMPricingCacheCosts struct {
|
||||
Mode LLMPricingRuleCacheMode `json:"mode" required:"true"`
|
||||
Read float64 `json:"read"`
|
||||
Write float64 `json:"write"`
|
||||
}
|
||||
|
||||
type LLMPricingRule struct {
|
||||
bun.BaseModel `bun:"table:llm_pricing_rule,alias:llm_pricing_rule" json:"-"`
|
||||
|
||||
types.Identifiable
|
||||
types.TimeAuditable
|
||||
types.UserAuditable
|
||||
|
||||
OrgID valuer.UUID `bun:"org_id,type:text,notnull" json:"orgId" required:"true"`
|
||||
SourceID *valuer.UUID `bun:"source_id,type:text" json:"sourceId,omitempty"`
|
||||
Model string `bun:"model,type:text,notnull" json:"modelName" required:"true"`
|
||||
Provider string `bun:"provider,type:text,notnull" json:"provider" required:"true"`
|
||||
ModelPattern StringSlice `bun:"model_pattern,type:text,notnull" json:"modelPattern" required:"true"`
|
||||
Unit LLMPricingRuleUnit `bun:"unit,type:text,notnull" json:"unit" required:"true"`
|
||||
Pricing LLMRulePricing `bun:"pricing,type:text,notnull,default:'{}'" json:"pricing" required:"true"`
|
||||
// IsOverride marks the row as user-pinned. When true, Zeus skips it entirely.
|
||||
IsOverride bool `bun:"is_override,notnull,default:false" json:"isOverride" required:"true"`
|
||||
SyncedAt *time.Time `bun:"synced_at" json:"syncedAt,omitempty"`
|
||||
Enabled bool `bun:"enabled,notnull,default:true" json:"enabled" required:"true"`
|
||||
}
|
||||
|
||||
type GettableLLMPricingRule = LLMPricingRule
|
||||
|
||||
type StorableLLMPricingRule = LLMPricingRule
|
||||
|
||||
// UpdatableLLMPricingRule is one entry in the bulk upsert batch.
|
||||
//
|
||||
// Identification:
|
||||
// - ID set → match by id (user editing a known row).
|
||||
// - SourceID set → match by source_id (Zeus sync, or user editing a Zeus-synced row).
|
||||
// - neither set → insert a new row with source_id = NULL (user-created custom rule).
|
||||
//
|
||||
// IsOverride is a pointer so the caller can distinguish "not sent" from "set to false".
|
||||
// When IsOverride is nil AND the matched row has is_override = true, the row is fully
|
||||
// preserved — only synced_at is stamped.
|
||||
type UpdatableLLMPricingRule struct {
|
||||
ID *valuer.UUID `json:"id,omitempty"`
|
||||
SourceID *valuer.UUID `json:"sourceId,omitempty"`
|
||||
Model string `json:"modelName" required:"true"`
|
||||
Provider string `json:"provider" required:"true"`
|
||||
ModelPattern []string `json:"modelPattern" required:"true"`
|
||||
Unit LLMPricingRuleUnit `json:"unit" required:"true"`
|
||||
Pricing LLMRulePricing `json:"pricing" required:"true"`
|
||||
IsOverride *bool `json:"isOverride,omitempty"`
|
||||
Enabled bool `json:"enabled" required:"true"`
|
||||
}
|
||||
|
||||
type UpdatableLLMPricingRules struct {
|
||||
Rules []UpdatableLLMPricingRule `json:"rules" required:"true"`
|
||||
}
|
||||
|
||||
type ListPricingRulesQuery struct {
|
||||
Offset int `query:"offset" json:"offset"`
|
||||
Limit int `query:"limit" json:"limit"`
|
||||
}
|
||||
|
||||
type GettablePricingRules struct {
|
||||
Items []*GettableLLMPricingRule `json:"items" required:"true"`
|
||||
Total int `json:"total" required:"true"`
|
||||
Offset int `json:"offset" required:"true"`
|
||||
Limit int `json:"limit" required:"true"`
|
||||
}
|
||||
|
||||
func (LLMPricingRuleUnit) Enum() []any {
|
||||
return []any{UnitPerMillionTokens}
|
||||
}
|
||||
|
||||
func (LLMPricingRuleCacheMode) Enum() []any {
|
||||
return []any{LLMPricingRuleCacheModeSubtract, LLMPricingRuleCacheModeAdditive, LLMPricingRuleCacheModeUnknown}
|
||||
}
|
||||
|
||||
func (s StringSlice) Value() (driver.Value, error) {
|
||||
if s == nil {
|
||||
return "[]", nil
|
||||
}
|
||||
b, err := json.Marshal(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (s *StringSlice) Scan(src any) error {
|
||||
var raw []byte
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
raw = []byte(v)
|
||||
case []byte:
|
||||
raw = v
|
||||
case nil:
|
||||
*s = nil
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInternalf(errors.CodeInternal, "llmpricingruletypes: cannot scan %T into StringSlice", src)
|
||||
}
|
||||
return json.Unmarshal(raw, s)
|
||||
}
|
||||
|
||||
func (p LLMRulePricing) Value() (driver.Value, error) {
|
||||
b, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func (p *LLMRulePricing) Scan(src any) error {
|
||||
var raw []byte
|
||||
switch v := src.(type) {
|
||||
case string:
|
||||
raw = []byte(v)
|
||||
case []byte:
|
||||
raw = v
|
||||
case nil:
|
||||
*p = LLMRulePricing{}
|
||||
return nil
|
||||
default:
|
||||
return errors.NewInternalf(errors.CodeInternal, "llmpricingruletypes: cannot scan %T into LLMRulePricing", src)
|
||||
}
|
||||
return json.Unmarshal(raw, p)
|
||||
}
|
||||
|
||||
func NewGettableLLMPricingRulesFromLLMPricingRules(items []*LLMPricingRule, total, offset, limit int) *GettablePricingRules {
|
||||
return &GettablePricingRules{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package llmpricingruletypes
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
List(ctx context.Context, orgID valuer.UUID, offset, limit int) ([]*StorableLLMPricingRule, int, error)
|
||||
Get(ctx context.Context, orgID, id valuer.UUID) (*StorableLLMPricingRule, error)
|
||||
GetBySourceID(ctx context.Context, orgID, sourceID valuer.UUID) (*StorableLLMPricingRule, error)
|
||||
Create(ctx context.Context, rule *StorableLLMPricingRule) error
|
||||
Update(ctx context.Context, rule *StorableLLMPricingRule) error
|
||||
Delete(ctx context.Context, orgID, id valuer.UUID) error
|
||||
}
|
||||
@@ -64,74 +64,6 @@ type QueryRangeResponse struct {
|
||||
QBEvent *QBEvent `json:"-"`
|
||||
}
|
||||
|
||||
// QueryRangePreviewResponse describes the dry-run output of a query range
|
||||
// request. Each entry corresponds to a single query in the composite query.
|
||||
type QueryRangePreviewResponse struct {
|
||||
Type RequestType `json:"type"`
|
||||
Statements []*PreviewStatement `json:"statements"`
|
||||
}
|
||||
|
||||
// ExplainVariant identifies one of the ClickHouse EXPLAIN modes that the
|
||||
// preview endpoint can run against a rendered SQL statement.
|
||||
type ExplainVariant string
|
||||
|
||||
const (
|
||||
ExplainVariantNone ExplainVariant = ""
|
||||
ExplainVariantPlan ExplainVariant = "plan"
|
||||
ExplainVariantAST ExplainVariant = "ast"
|
||||
ExplainVariantSyntax ExplainVariant = "syntax"
|
||||
ExplainVariantPipeline ExplainVariant = "pipeline"
|
||||
ExplainVariantEstimate ExplainVariant = "estimate"
|
||||
ExplainVariantQueryTree ExplainVariant = "query_tree"
|
||||
)
|
||||
|
||||
// IsValid reports whether the variant is one of the supported values
|
||||
// (including the empty/no-explain sentinel).
|
||||
func (v ExplainVariant) IsValid() bool {
|
||||
switch v {
|
||||
case ExplainVariantNone,
|
||||
ExplainVariantPlan,
|
||||
ExplainVariantAST,
|
||||
ExplainVariantSyntax,
|
||||
ExplainVariantPipeline,
|
||||
ExplainVariantEstimate,
|
||||
ExplainVariantQueryTree:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// QueryRangePreviewOptions carries per-call options for the query range
|
||||
// preview (dry-run) endpoint. The zero value is meaningful and produces a
|
||||
// validation-only preview with no EXPLAIN.
|
||||
type QueryRangePreviewOptions struct {
|
||||
// Explain selects which ClickHouse EXPLAIN variant to run for each
|
||||
// rendered SQL statement. Leave empty to skip EXPLAIN.
|
||||
Explain ExplainVariant
|
||||
}
|
||||
|
||||
// PrepareJSONSchema adds description to the QueryRangePreviewResponse schema.
|
||||
func (q *QueryRangePreviewResponse) PrepareJSONSchema(schema *jsonschema.Schema) error {
|
||||
schema.WithDescription("Response from the v5 query range preview (dry-run) endpoint. Returns the rendered SQL/PromQL for each query in the composite query along with optional EXPLAIN output when requested.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// PreviewStatement is the rendered form of a single query.
|
||||
type PreviewStatement struct {
|
||||
QueryName string `json:"queryName"`
|
||||
QueryType string `json:"queryType"`
|
||||
Query string `json:"query"`
|
||||
Args []any `json:"args,omitempty"`
|
||||
Error error `json:"error"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
// ExplainVariant is the EXPLAIN mode that produced Explain. Empty when
|
||||
// no EXPLAIN was requested.
|
||||
ExplainVariant string `json:"explainVariant,omitempty"`
|
||||
// Explain is the formatted output returned by ClickHouse for the
|
||||
// requested EXPLAIN variant. Empty when no EXPLAIN was requested.
|
||||
Explain string `json:"explain,omitempty"`
|
||||
}
|
||||
|
||||
var _ jsonschema.Preparer = &QueryRangeResponse{}
|
||||
|
||||
// PrepareJSONSchema adds description to the QueryRangeResponse schema.
|
||||
|
||||
Reference in New Issue
Block a user