Compare commits

..

1 Commits

Author SHA1 Message Date
aks07
d4cd6a9687 feat: add shared reusable components for trace details v3
Adds reusable components to periscope and DetailsPanel:

- PrettyView: JSON tree viewer with search and pinning
- JsonView: Monaco-based JSON viewer
- DataViewer: Pretty/JSON mode switcher
- FloatingPanel: Draggable/resizable panel (react-rnd)
- ResizableBox: Vertical/horizontal resize with handle
- KeyValueLabel: Key-value display (column/row)
- DetailsPanel: Drawer infrastructure and header
2026-04-10 09:35:55 +05:30
48 changed files with 1719 additions and 1309 deletions

View File

@@ -1,169 +1,5 @@
components:
schemas:
Aio11YtypesCacheMode:
enum:
- subtract
- additive
- unknown
nullable: true
type: string
Aio11YtypesGettablePricingRule:
properties:
cache_mode:
$ref: '#/components/schemas/Aio11YtypesCacheMode'
cost_cache_read:
format: double
type: number
cost_cache_write:
format: double
type: number
cost_input:
format: double
type: number
cost_output:
format: double
type: number
created_at:
format: date-time
type: string
created_by:
type: string
enabled:
type: boolean
id:
type: string
is_override:
type: boolean
model_name:
type: string
model_pattern:
items:
type: string
nullable: true
type: array
synced_at:
format: date-time
nullable: true
type: string
unit:
$ref: '#/components/schemas/Aio11YtypesUnit'
updated_at:
format: date-time
type: string
updated_by:
type: string
required:
- id
- model_name
- model_pattern
- unit
- cache_mode
- cost_input
- cost_output
- cost_cache_read
- cost_cache_write
- is_override
- enabled
- created_at
- updated_at
- created_by
- updated_by
type: object
Aio11YtypesListPricingRulesResponse:
properties:
items:
items:
$ref: '#/components/schemas/Aio11YtypesGettablePricingRule'
nullable: true
type: array
limit:
type: integer
offset:
type: integer
total:
type: integer
required:
- items
- total
- offset
- limit
type: object
Aio11YtypesPostablePricingRule:
properties:
cache_mode:
$ref: '#/components/schemas/Aio11YtypesCacheMode'
cost_cache_read:
format: double
type: number
cost_cache_write:
format: double
type: number
cost_input:
format: double
type: number
cost_output:
format: double
type: number
enabled:
type: boolean
model_name:
type: string
model_pattern:
items:
type: string
nullable: true
type: array
unit:
$ref: '#/components/schemas/Aio11YtypesUnit'
required:
- model_name
- model_pattern
- unit
- cache_mode
- cost_input
- cost_output
- cost_cache_read
- cost_cache_write
- enabled
type: object
Aio11YtypesUnit:
enum:
- per_million_tokens
- per_token
nullable: true
type: string
Aio11YtypesUpdatablePricingRule:
properties:
cache_mode:
$ref: '#/components/schemas/Aio11YtypesCacheMode'
cost_cache_read:
nullable: true
type: number
cost_cache_write:
nullable: true
type: number
cost_input:
nullable: true
type: number
cost_output:
nullable: true
type: number
enabled:
nullable: true
type: boolean
is_override:
nullable: true
type: boolean
model_name:
nullable: true
type: string
model_pattern:
items:
type: string
type: array
unit:
$ref: '#/components/schemas/Aio11YtypesUnit'
type: object
AuthtypesAttributeMapping:
properties:
email:
@@ -3222,303 +3058,6 @@ info:
version: ""
openapi: 3.0.3
paths:
/api/v1/ai-o11y/pricing/rules:
get:
deprecated: false
description: Returns all LLM pricing rules for the authenticated org, with pagination.
operationId: ListPricingRules
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/Aio11YtypesListPricingRulesResponse'
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:
- ai-o11y
post:
deprecated: false
description: Creates a new LLM pricing rule for the org. Always sets is_override
= true.
operationId: CreatePricingRule
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Aio11YtypesPostablePricingRule'
responses:
"201":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Aio11YtypesGettablePricingRule'
status:
type: string
required:
- status
- data
type: object
description: Created
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Create a pricing rule
tags:
- ai-o11y
/api/v1/ai-o11y/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: DeletePricingRule
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:
- ai-o11y
get:
deprecated: false
description: Returns a single LLM pricing rule by ID.
operationId: GetPricingRule
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Aio11YtypesGettablePricingRule'
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:
- ai-o11y
put:
deprecated: false
description: Partially updates an existing pricing rule. Changing any cost field
sets is_override = true.
operationId: UpdatePricingRule
parameters:
- in: path
name: id
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Aio11YtypesUpdatablePricingRule'
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Aio11YtypesGettablePricingRule'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- ADMIN
- tokenizer:
- ADMIN
summary: Update a pricing rule
tags:
- ai-o11y
/api/v1/authz/check:
post:
deprecated: false
@@ -3770,7 +3309,7 @@ paths:
schema:
$ref: '#/components/schemas/CloudintegrationtypesPostableAccount'
responses:
"201":
"200":
content:
application/json:
schema:
@@ -3783,7 +3322,7 @@ paths:
- status
- data
type: object
description: Created
description: OK
"401":
content:
application/json:
@@ -4144,11 +3683,6 @@ paths:
provider
operationId: ListServicesMetadata
parameters:
- in: query
name: cloud_integration_id
required: false
schema:
type: string
- in: path
name: cloud_provider
required: true
@@ -4201,11 +3735,6 @@ paths:
description: This endpoint gets a service for the specified cloud provider
operationId: GetService
parameters:
- in: query
name: cloud_integration_id
required: false
schema:
type: string
- in: path
name: cloud_provider
required: true

View File

@@ -139,10 +139,12 @@
"react-helmet-async": "1.3.0",
"react-hook-form": "7.71.2",
"react-i18next": "^11.16.1",
"react-json-tree": "0.20.0",
"react-lottie": "1.2.10",
"react-markdown": "8.0.7",
"react-query": "3.39.3",
"react-redux": "^7.2.2",
"react-rnd": "10.5.3",
"react-router-dom": "^5.2.0",
"react-router-dom-v5-compat": "6.27.0",
"react-syntax-highlighter": "15.5.0",

View File

@@ -28,7 +28,7 @@ import type {
CloudintegrationtypesPostableAgentCheckInDTO,
CloudintegrationtypesUpdatableAccountDTO,
CloudintegrationtypesUpdatableServiceDTO,
CreateAccount201,
CreateAccount200,
CreateAccountPathParameters,
DisconnectAccountPathParameters,
GetAccount200,
@@ -36,12 +36,10 @@ import type {
GetConnectionCredentials200,
GetConnectionCredentialsPathParameters,
GetService200,
GetServiceParams,
GetServicePathParameters,
ListAccounts200,
ListAccountsPathParameters,
ListServicesMetadata200,
ListServicesMetadataParams,
ListServicesMetadataPathParameters,
RenderErrorResponseDTO,
UpdateAccountPathParameters,
@@ -262,7 +260,7 @@ export const createAccount = (
cloudintegrationtypesPostableAccountDTO: BodyType<CloudintegrationtypesPostableAccountDTO>,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<CreateAccount201>({
return GeneratedAPIInstance<CreateAccount200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/accounts`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -942,25 +940,19 @@ export const invalidateGetConnectionCredentials = async (
*/
export const listServicesMetadata = (
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<ListServicesMetadata200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/services`,
method: 'GET',
params,
signal,
});
};
export const getListServicesMetadataQueryKey = (
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/services`,
...(params ? [params] : []),
] as const;
export const getListServicesMetadataQueryKey = ({
cloudProvider,
}: ListServicesMetadataPathParameters) => {
return [`/api/v1/cloud_integrations/${cloudProvider}/services`] as const;
};
export const getListServicesMetadataQueryOptions = <
@@ -968,7 +960,6 @@ export const getListServicesMetadataQueryOptions = <
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServicesMetadata>>,
@@ -980,12 +971,11 @@ export const getListServicesMetadataQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getListServicesMetadataQueryKey({ cloudProvider }, params);
queryOptions?.queryKey ?? getListServicesMetadataQueryKey({ cloudProvider });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listServicesMetadata>>
> = ({ signal }) => listServicesMetadata({ cloudProvider }, params, signal);
> = ({ signal }) => listServicesMetadata({ cloudProvider }, signal);
return {
queryKey,
@@ -1013,7 +1003,6 @@ export function useListServicesMetadata<
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof listServicesMetadata>>,
@@ -1024,7 +1013,6 @@ export function useListServicesMetadata<
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getListServicesMetadataQueryOptions(
{ cloudProvider },
params,
options,
);
@@ -1043,11 +1031,10 @@ export function useListServicesMetadata<
export const invalidateListServicesMetadata = async (
queryClient: QueryClient,
{ cloudProvider }: ListServicesMetadataPathParameters,
params?: ListServicesMetadataParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }, params) },
{ queryKey: getListServicesMetadataQueryKey({ cloudProvider }) },
options,
);
@@ -1060,24 +1047,21 @@ export const invalidateListServicesMetadata = async (
*/
export const getService = (
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetService200>({
url: `/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
method: 'GET',
params,
signal,
});
};
export const getGetServiceQueryKey = (
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
) => {
export const getGetServiceQueryKey = ({
cloudProvider,
serviceId,
}: GetServicePathParameters) => {
return [
`/api/v1/cloud_integrations/${cloudProvider}/services/${serviceId}`,
...(params ? [params] : []),
] as const;
};
@@ -1086,7 +1070,6 @@ export const getGetServiceQueryOptions = <
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getService>>,
@@ -1098,12 +1081,11 @@ export const getGetServiceQueryOptions = <
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetServiceQueryKey({ cloudProvider, serviceId }, params);
queryOptions?.queryKey ?? getGetServiceQueryKey({ cloudProvider, serviceId });
const queryFn: QueryFunction<Awaited<ReturnType<typeof getService>>> = ({
signal,
}) => getService({ cloudProvider, serviceId }, params, signal);
}) => getService({ cloudProvider, serviceId }, signal);
return {
queryKey,
@@ -1129,7 +1111,6 @@ export function useGetService<
TError = ErrorType<RenderErrorResponseDTO>
>(
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getService>>,
@@ -1140,7 +1121,6 @@ export function useGetService<
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetServiceQueryOptions(
{ cloudProvider, serviceId },
params,
options,
);
@@ -1159,11 +1139,10 @@ export function useGetService<
export const invalidateGetService = async (
queryClient: QueryClient,
{ cloudProvider, serviceId }: GetServicePathParameters,
params?: GetServiceParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }, params) },
{ queryKey: getGetServiceQueryKey({ cloudProvider, serviceId }) },
options,
);

View File

@@ -3589,7 +3589,7 @@ export type ListAccounts200 = {
export type CreateAccountPathParameters = {
cloudProvider: string;
};
export type CreateAccount201 = {
export type CreateAccount200 = {
data: CloudintegrationtypesGettableAccountWithConnectionArtifactDTO;
/**
* @type string
@@ -3647,14 +3647,6 @@ export type GetConnectionCredentials200 = {
export type ListServicesMetadataPathParameters = {
cloudProvider: string;
};
export type ListServicesMetadataParams = {
/**
* @type string
* @description undefined
*/
cloud_integration_id?: string;
};
export type ListServicesMetadata200 = {
data: CloudintegrationtypesGettableServicesMetadataDTO;
/**
@@ -3667,14 +3659,6 @@ export type GetServicePathParameters = {
cloudProvider: string;
serviceId: string;
};
export type GetServiceParams = {
/**
* @type string
* @description undefined
*/
cloud_integration_id?: string;
};
export type GetService200 = {
data: CloudintegrationtypesServiceDTO;
/**

View File

@@ -0,0 +1,40 @@
.details-header {
// ghost + secondary missing hover bg token in @signozhq/button
--button-ghost-hover-background: var(--l3-background);
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
border-bottom: 1px solid var(--l1-border);
height: 36px;
background: var(--l2-background);
&__icon-btn {
flex-shrink: 0;
}
&__title {
font-size: 13px;
font-weight: 500;
color: var(--l1-foreground);
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__actions {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
&__nav {
display: flex;
align-items: center;
gap: 2px;
}
}

View File

@@ -0,0 +1,59 @@
import { ReactNode } from 'react';
import { Button } from '@signozhq/button';
import { X } from '@signozhq/icons';
import './DetailsHeader.styles.scss';
export interface HeaderAction {
key: string;
component: ReactNode; // check later if we can use direct btn itself or not.
}
export interface DetailsHeaderProps {
title: string;
onClose: () => void;
actions?: HeaderAction[];
closePosition?: 'left' | 'right';
className?: string;
}
function DetailsHeader({
title,
onClose,
actions,
closePosition = 'right',
className,
}: DetailsHeaderProps): JSX.Element {
const closeButton = (
<Button
variant="ghost"
size="icon"
color="secondary"
onClick={onClose}
aria-label="Close"
className="details-header__icon-btn"
>
<X size={14} />
</Button>
);
return (
<div className={`details-header ${className || ''}`}>
{closePosition === 'left' && closeButton}
<span className="details-header__title">{title}</span>
{actions && (
<div className="details-header__actions">
{actions.map((action) => (
<div key={action.key}>{action.component}</div>
))}
</div>
)}
{closePosition === 'right' && closeButton}
</div>
);
}
export default DetailsHeader;

View File

@@ -0,0 +1,7 @@
.details-panel-drawer {
&__body {
display: flex;
flex-direction: column;
height: 100%;
}
}

View File

@@ -0,0 +1,36 @@
import { DrawerWrapper } from '@signozhq/drawer';
import './DetailsPanelDrawer.styles.scss';
interface DetailsPanelDrawerProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
className?: string;
}
function DetailsPanelDrawer({
isOpen,
onClose,
children,
className,
}: DetailsPanelDrawerProps): JSX.Element {
return (
<DrawerWrapper
open={isOpen}
onOpenChange={(open): void => {
if (!open) {
onClose();
}
}}
direction="right"
type="panel"
showOverlay={false}
allowOutsideClick
className={`details-panel-drawer ${className || ''}`}
content={<div className="details-panel-drawer__body">{children}</div>}
/>
);
}
export default DetailsPanelDrawer;

View File

@@ -0,0 +1,8 @@
export type {
DetailsHeaderProps,
HeaderAction,
} from './DetailsHeader/DetailsHeader';
export { default as DetailsHeader } from './DetailsHeader/DetailsHeader';
export { default as DetailsPanelDrawer } from './DetailsPanelDrawer';
export type { DetailsPanelState, UseDetailsPanelOptions } from './types';
export { default as useDetailsPanel } from './useDetailsPanel';

View File

@@ -0,0 +1,10 @@
export interface DetailsPanelState {
isOpen: boolean;
open: () => void;
close: () => void;
}
export interface UseDetailsPanelOptions {
entityId: string | undefined;
onClose?: () => void;
}

View File

@@ -0,0 +1,29 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { DetailsPanelState, UseDetailsPanelOptions } from './types';
function useDetailsPanel({
entityId,
onClose,
}: UseDetailsPanelOptions): DetailsPanelState {
const [isOpen, setIsOpen] = useState<boolean>(false);
const prevEntityIdRef = useRef<string>('');
useEffect(() => {
const currentId = entityId || '';
if (currentId && currentId !== prevEntityIdRef.current) {
setIsOpen(true);
}
prevEntityIdRef.current = currentId;
}, [entityId]);
const open = useCallback(() => setIsOpen(true), []);
const close = useCallback(() => {
setIsOpen(false);
onClose?.();
}, [onClose]);
return { isOpen, open, close };
}
export default useDetailsPanel;

View File

@@ -0,0 +1,54 @@
.data-viewer {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
// Toolbar — view mode switcher + copy button
&__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 8px;
}
&__mode-select {
min-width: 90px;
height: 28px !important;
.ant-select-selector {
border-radius: 4px !important;
border-color: var(--l2-border) !important;
font-size: 12px !important;
}
}
&__copy-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 4px 8px;
border: 1px solid var(--l2-border);
border-radius: 4px;
background: transparent;
color: var(--l2-foreground);
cursor: pointer;
&:hover {
color: var(--l1-foreground);
background: var(--l3-background);
}
}
// Shared content container — no scroll, each view handles its own
&__content {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid var(--l2-border);
border-radius: 4px;
padding: 8px;
min-height: 0;
overflow: hidden;
}
}

View File

@@ -0,0 +1,67 @@
import { useMemo, useState } from 'react';
import { Copy } from '@signozhq/icons';
// TODO: Replace antd Select with @signozhq/ui component when moving to design library
import { Select } from 'antd';
import { JsonView } from 'periscope/components/JsonView';
import { PrettyView } from 'periscope/components/PrettyView';
import { PrettyViewProps } from 'periscope/components/PrettyView';
import { copyToClipboard } from './utils';
import './DataViewer.styles.scss';
type ViewMode = 'pretty' | 'json';
export interface DataViewerProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: Record<string, any>;
drawerKey?: string;
prettyViewProps?: Omit<PrettyViewProps, 'data' | 'drawerKey'>;
}
function DataViewer({
data,
drawerKey = 'default',
prettyViewProps,
}: DataViewerProps): JSX.Element {
const [viewMode, setViewMode] = useState<ViewMode>('pretty');
const jsonString = useMemo(() => JSON.stringify(data, null, 2), [data]);
return (
<div className="data-viewer">
<div className="data-viewer__toolbar">
<Select
className="data-viewer__mode-select"
size="small"
value={viewMode}
onChange={(value: ViewMode): void => setViewMode(value)}
options={[
{ label: 'Pretty', value: 'pretty' },
{ label: 'JSON', value: 'json' },
]}
getPopupContainer={(trigger): HTMLElement =>
trigger.parentElement || document.body
}
/>
<button
type="button"
className="data-viewer__copy-btn"
onClick={(): void => copyToClipboard(data)}
aria-label="Copy JSON"
>
<Copy size={14} />
</button>
</div>
<div className="data-viewer__content">
{viewMode === 'pretty' && (
<PrettyView data={data} drawerKey={drawerKey} {...prettyViewProps} />
)}
{viewMode === 'json' && <JsonView data={jsonString} />}
</div>
</div>
);
}
export default DataViewer;

View File

@@ -0,0 +1,2 @@
export type { DataViewerProps } from './DataViewer';
export { default as DataViewer } from './DataViewer';

View File

@@ -0,0 +1,12 @@
import { toast } from '@signozhq/sonner';
export function copyToClipboard(value: unknown): void {
const text =
typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
// eslint-disable-next-line no-restricted-properties
navigator.clipboard.writeText(text);
toast.success('Copied to clipboard', {
richColors: true,
position: 'top-right',
});
}

View File

@@ -0,0 +1,22 @@
.floating-panel {
z-index: 999;
background: var(--l1-background);
border: 1px solid var(--l2-border);
border-radius: 6px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
overflow: hidden;
// Inner div fills the Rnd-controlled size
&__inner {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
// Drag handle — any child with this class becomes the drag target
.floating-panel__drag-handle {
cursor: move;
}
}

View File

@@ -0,0 +1,80 @@
import { ComponentProps } from 'react';
import { createPortal } from 'react-dom';
import { Rnd } from 'react-rnd';
import './FloatingPanel.styles.scss';
type EnableResizing = ComponentProps<typeof Rnd>['enableResizing'];
export interface FloatingPanelProps {
isOpen: boolean;
children: React.ReactNode;
defaultPosition?: { x: number; y: number };
width?: number;
height?: number;
minWidth?: number;
minHeight?: number;
enableResizing?: EnableResizing;
className?: string;
}
function FloatingPanel({
isOpen,
children,
defaultPosition,
width = 560,
height = 600,
minWidth = 400,
minHeight = 300,
enableResizing,
className,
}: FloatingPanelProps): JSX.Element | null {
if (!isOpen) {
return null;
}
const initialPosition = defaultPosition || {
x: window.innerWidth - width - 24,
y: 80,
};
return createPortal(
<Rnd
default={{
x: initialPosition.x,
y: initialPosition.y,
width,
height,
}}
dragHandleClassName="floating-panel__drag-handle"
minWidth={minWidth}
minHeight={minHeight}
onDrag={(_e, d): void | false => {
const HEADER_HEIGHT = 40;
// Top: don't allow header to go above viewport
if (d.y < 0) {
return false;
}
// Left: don't allow panel to go off-screen left
if (d.x < 0) {
return false;
}
// Bottom: only header needs to be visible
if (d.y > window.innerHeight - HEADER_HEIGHT) {
return false;
}
// Right: at least the close button (~40px) stays visible
if (d.x > window.innerWidth - 40) {
return false;
}
}}
className={`floating-panel ${className || ''}`}
enableResizing={enableResizing}
>
<div className="floating-panel__inner">{children}</div>
</Rnd>,
document.body,
);
}
export default FloatingPanel;

View File

@@ -0,0 +1,2 @@
export type { FloatingPanelProps } from './FloatingPanel';
export { default as FloatingPanel } from './FloatingPanel';

View File

@@ -0,0 +1,22 @@
.json-view {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
&__footer {
flex-shrink: 0;
height: 36px;
display: flex;
align-items: center;
border-top: 1px solid var(--l2-border);
}
&__wrap-toggle {
display: flex;
gap: 8px;
margin-left: 12px;
align-items: center;
}
}

View File

@@ -0,0 +1,78 @@
import { useState } from 'react';
import MEditor, { EditorProps, Monaco } from '@monaco-editor/react';
import { Color } from '@signozhq/design-tokens';
import { Switch, Typography } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import './JsonView.styles.scss';
export interface JsonViewProps {
data: string;
height?: string;
}
const editorOptions: EditorProps['options'] = {
automaticLayout: true,
readOnly: true,
wordWrap: 'on',
minimap: { enabled: false },
fontWeight: 400,
fontFamily: 'SF Mono, Geist Mono, Fira Code, monospace',
fontSize: 12,
lineHeight: '18px',
colorDecorators: true,
scrollBeyondLastLine: false,
decorationsOverviewRuler: false,
scrollbar: { vertical: 'hidden', horizontal: 'hidden' },
folding: false,
};
function setEditorTheme(monaco: Monaco): void {
monaco.editor.defineTheme('signoz-dark', {
base: 'vs-dark',
inherit: true,
rules: [
{ token: 'string.key.json', foreground: Color.BG_VANILLA_400 },
{ token: 'string.value.json', foreground: Color.BG_ROBIN_400 },
],
colors: {
'editor.background': '#00000000', // transparent
},
fontFamily: 'SF Mono, Geist Mono, Fira Code, monospace',
fontSize: 12,
fontWeight: 'normal',
lineHeight: 18,
letterSpacing: -0.06,
});
}
function JsonView({ data, height = '575px' }: JsonViewProps): JSX.Element {
const [isWrapWord, setIsWrapWord] = useState(true);
const isDarkMode = useIsDarkMode();
return (
<div className="json-view">
<MEditor
value={data}
language="json"
options={{ ...editorOptions, wordWrap: isWrapWord ? 'on' : 'off' }}
onChange={(): void => {}}
height={height}
theme={isDarkMode ? 'signoz-dark' : 'light'}
beforeMount={setEditorTheme}
/>
<div className="json-view__footer">
<div className="json-view__wrap-toggle">
<Typography.Text>Wrap text</Typography.Text>
<Switch
checked={isWrapWord}
onChange={(checked): void => setIsWrapWord(checked)}
size="small"
/>
</div>
</div>
</div>
);
}
export default JsonView;

View File

@@ -0,0 +1,2 @@
export type { JsonViewProps } from './JsonView';
export { default as JsonView } from './JsonView';

View File

@@ -1,43 +1,67 @@
.key-value-label {
display: flex;
align-items: center;
border: 1px solid var(--bg-slate-400);
border: 1px solid var(--l2-border);
border-radius: 2px;
flex-wrap: wrap;
&--row {
align-items: center;
flex-direction: row;
flex-wrap: wrap;
}
&--column {
flex-direction: column;
border: none;
border-radius: 0;
gap: 4px;
padding: 8px;
.key-value-label__key {
background: transparent;
border-radius: 0;
padding: 0;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.48px;
line-height: 20px;
}
.key-value-label__value {
background: transparent;
padding: 0;
font-size: 14px;
line-height: 20px;
}
}
&__key,
&__value {
padding: 1px 6px;
padding: 6px;
font-size: 14px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.005em;
&,
&:hover {
color: var(--text-vanilla-400);
color: var(--l1-foreground);
}
}
&__key {
background: var(--bg-ink-400);
background: var(--l2-background);
border-radius: 2px 0 0 2px;
}
&__value {
background: var(--bg-slate-400);
}
}
.lightMode {
.key-value-label {
border-color: var(--bg-vanilla-400);
&__key,
&__value {
color: var(--text-ink-400);
}
&__key {
background: var(--bg-vanilla-300);
}
&__value {
background: var(--bg-vanilla-200);
&__value {
background: var(--l3-background);
&--clickable {
cursor: pointer;
&:hover {
opacity: 0.8;
}
}
}
}

View File

@@ -1,29 +1,61 @@
import { useMemo } from 'react';
import { Tooltip } from 'antd';
import TrimmedText from '../TrimmedText/TrimmedText';
import './KeyValueLabel.styles.scss';
// Rethink this component later
type KeyValueLabelProps = {
badgeKey: string | React.ReactNode;
badgeValue: string;
badgeValue: string | React.ReactNode;
direction?: 'row' | 'column';
maxCharacters?: number;
onClick?: () => void;
};
export default function KeyValueLabel({
badgeKey,
badgeValue,
direction = 'row',
maxCharacters = 20,
onClick,
}: KeyValueLabelProps): JSX.Element | null {
const isUrl = useMemo(() => /^https?:\/\//.test(badgeValue), [badgeValue]);
if (!badgeKey || !badgeValue) {
return null;
}
const renderValue = (): JSX.Element => {
if (typeof badgeValue !== 'string') {
return <div className="key-value-label__value">{badgeValue}</div>;
}
return (
<Tooltip title={badgeValue}>
<div
className={`key-value-label__value ${
onClick ? 'key-value-label__value--clickable' : ''
}`}
onClick={onClick}
role={onClick ? 'button' : undefined}
tabIndex={onClick ? 0 : undefined}
onKeyDown={
onClick
? (e): void => {
if (e.key === 'Enter' || e.key === ' ') {
onClick();
}
}
: undefined
}
>
<TrimmedText text={badgeValue} maxCharacters={maxCharacters} />
</div>
</Tooltip>
);
};
return (
<div className="key-value-label">
<div className={`key-value-label key-value-label--${direction}`}>
<div className="key-value-label__key">
{typeof badgeKey === 'string' ? (
<TrimmedText text={badgeKey} maxCharacters={maxCharacters} />
@@ -31,26 +63,13 @@ export default function KeyValueLabel({
badgeKey
)}
</div>
{isUrl ? (
<a
href={badgeValue}
target="_blank"
rel="noopener noreferrer"
className="key-value-label__value"
>
<TrimmedText text={badgeValue} maxCharacters={maxCharacters} />
</a>
) : (
<Tooltip title={badgeValue}>
<div className="key-value-label__value">
<TrimmedText text={badgeValue} maxCharacters={maxCharacters} />
</div>
</Tooltip>
)}
{renderValue()}
</div>
);
}
KeyValueLabel.defaultProps = {
maxCharacters: 20,
direction: 'row',
onClick: undefined,
};

View File

@@ -0,0 +1,191 @@
.pretty-view {
padding: 0;
flex: 1;
overflow-y: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
&__search-input {
position: sticky;
top: 0;
z-index: 1;
margin-bottom: 8px;
width: 100%;
border: 1px solid var(--l2-border) !important;
border-radius: 4px;
font-family: 'SF Mono', 'Geist Mono', 'Fira Code', monospace !important;
font-size: 12px !important;
line-height: 18px !important;
background: var(--l1-background) !important;
outline: none !important;
box-shadow: none !important;
&:focus {
border-color: var(--primary) !important;
outline: none !important;
box-shadow: none !important;
}
}
// Font spec: SF Mono, 12px, 400, 18px line-height, -0.5% letter-spacing
font-family: 'SF Mono', 'Geist Mono', 'Fira Code', monospace;
font-size: 12px;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.06px;
// Override react-json-tree inline styles
ul {
margin: 0 !important;
padding-left: 16px !important;
list-style: none !important;
background-color: transparent !important;
flex: 1;
}
> ul,
&__pinned > ul {
padding-left: 0 !important;
}
// Force font on all tree elements
li,
span,
label {
font-family: inherit !important;
font-size: inherit !important;
line-height: inherit !important;
letter-spacing: inherit !important;
}
// Fix react-json-tree text-indent wrapping
li {
text-indent: 0 !important;
margin-left: 0 !important;
padding-top: 2px !important;
padding-bottom: 2px !important;
}
.pretty-view__actions {
margin-right: 16px;
}
// Leaf node row — hover highlights only this row
&__row {
border-radius: 2px;
display: flex !important;
align-items: baseline;
&:hover {
background-color: var(--l3-background);
.pretty-view__actions {
opacity: 1;
}
}
// Push actions to the right edge
> span {
flex: 1;
}
}
// Nested node (object/array) — hover only on the label line, not children
&__nested-row {
> label,
> span:not(ul span) {
border-radius: 2px;
padding: 1px 2px;
display: inline !important; // keep item string inline with label
}
// Highlight label + item string on hover, show actions
&:hover > label,
&:hover > label + span {
background-color: var(--l3-background);
}
&:hover > label + span .pretty-view__actions {
opacity: 1;
}
// In nested rows, value-row should not take full width
.pretty-view__value-row {
width: auto;
}
}
// Value + actions wrapper (from valueRenderer / getItemString)
&__value-row {
display: inline-flex;
align-items: center;
gap: 6px;
}
// In leaf rows, value-row takes full width to push ... to the right
&__row &__value-row {
justify-content: space-between;
width: 100%;
}
// ... actions button — hidden by default, shown on row hover
&__actions {
opacity: 0;
display: inline-flex;
align-items: center;
cursor: pointer;
color: var(--l2-foreground);
transition: opacity 0.1s ease;
padding: 0 2px;
&:hover {
color: var(--l1-foreground);
}
}
// Pinned items section
&__pinned {
border-bottom: 1px solid var(--l2-border);
padding-bottom: 8px;
margin-bottom: 8px;
padding-left: 0px;
li {
margin-left: 0 !important;
padding-left: 0 !important;
display: flex !important;
align-items: baseline;
}
}
&__pinned-header {
font-size: 11px;
font-weight: 500;
color: var(--l3-foreground);
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 4px;
}
&__pinned-label {
display: inline-flex;
align-items: baseline;
gap: 6px;
}
&__pinned-icon {
flex-shrink: 0;
color: var(--text-robin-400);
cursor: pointer;
fill: var(--text-robin-400);
transition: fill 0.1s ease;
&:hover {
color: var(--text-robin-300);
fill: transparent;
}
}
}

View File

@@ -0,0 +1,300 @@
import { useCallback, useMemo } from 'react';
import { JSONTree, KeyPath } from 'react-json-tree';
import { Copy, Ellipsis, Pin, PinOff } from '@signozhq/icons';
import { Input } from '@signozhq/input';
import type { MenuProps } from 'antd';
// TODO: Replace antd Dropdown with @signozhq/ui component when moving to design library
import { Dropdown } from 'antd';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { darkTheme, lightTheme, themeExtension } from './constants';
import usePinnedFields from './hooks/usePinnedFields';
import useSearchFilter, { filterTree } from './hooks/useSearchFilter';
import {
copyToClipboard,
keyPathToDisplayString,
keyPathToForward,
serializeKeyPath,
} from './utils';
import './PrettyView.styles.scss';
export interface FieldContext {
fieldKey: string;
fieldKeyPath: (string | number)[];
fieldValue: unknown;
isNested: boolean;
}
export interface PrettyViewAction {
key: string;
label: React.ReactNode;
icon?: React.ReactNode;
onClick: (context: FieldContext) => void;
}
export interface PrettyViewProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: Record<string, any>;
actions?: PrettyViewAction[];
searchable?: boolean;
showPinned?: boolean;
drawerKey?: string;
}
function PrettyView({
data,
actions,
searchable = true,
showPinned = false,
drawerKey = 'default',
}: PrettyViewProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const { searchQuery, setSearchQuery, filteredData } = useSearchFilter(data);
const {
isPinned,
togglePin,
pinnedEntries,
pinnedData,
displayKeyToForwardPath,
} = usePinnedFields(data, drawerKey);
const filteredPinnedData = useMemo(() => {
const trimmed = searchQuery.trim();
if (!trimmed) {
return pinnedData;
}
return filterTree(pinnedData, trimmed) || {};
}, [pinnedData, searchQuery]);
const theme = useMemo(
() => ({
extend: isDarkMode ? darkTheme : lightTheme,
...themeExtension,
}),
[isDarkMode],
);
const shouldExpandNodeInitially = useCallback(
(
_keyPath: readonly (string | number)[],
_data: unknown,
level: number,
): boolean => level < 5,
[],
);
const buildMenuItems = useCallback(
(context: FieldContext): MenuProps['items'] => {
// todo: drive dropdown through config.
const copyItem = {
key: 'copy',
label: 'Copy',
icon: <Copy size={12} />,
onClick: (): void => {
copyToClipboard(context.fieldValue);
},
};
const items: NonNullable<MenuProps['items']> = [copyItem];
// Pin action only for leaf nodes
if (!context.isNested) {
// Resolve the correct forward path — pinned tree uses display keys
// which don't match the original serialized path
const resolvedPath =
displayKeyToForwardPath[context.fieldKey] || context.fieldKeyPath;
const serialized = serializeKeyPath(resolvedPath);
const pinned = isPinned(serialized);
items.push({
key: 'pin',
label: pinned ? 'Unpin field' : 'Pin field',
icon: pinned ? <PinOff size={12} /> : <Pin size={12} />,
onClick: (): void => {
togglePin(resolvedPath);
},
});
}
if (actions && actions.length > 0) {
//todo: why this divider?
items.push({ type: 'divider' as const, key: 'divider' });
actions.forEach((action) => {
items.push({
key: action.key,
label: action.label,
icon: action.icon,
onClick: (): void => {
action.onClick(context);
},
});
});
}
return items;
},
[actions, isPinned, togglePin, displayKeyToForwardPath],
);
const renderWithActions = useCallback(
({
content,
fieldKey,
fieldKeyPath,
value,
isNested,
}: {
content: React.ReactNode;
fieldKey: string;
fieldKeyPath: (string | number)[];
value: unknown;
isNested: boolean;
}): React.ReactNode => {
const context: FieldContext = {
fieldKey,
fieldKeyPath,
fieldValue: value,
isNested,
};
const menuItems = buildMenuItems(context);
return (
<span className="pretty-view__value-row">
<span>{content}</span>
<Dropdown
menu={{
items: menuItems,
onClick: (e): void => {
e.domEvent.stopPropagation();
},
}}
trigger={['click']}
placement="bottomLeft"
getPopupContainer={(trigger): HTMLElement =>
trigger.parentElement || document.body
}
>
<span
className="pretty-view__actions"
onClick={(e): void => e.stopPropagation()}
role="button"
tabIndex={0}
>
<Ellipsis size={12} />
</span>
</Dropdown>
</span>
);
},
[buildMenuItems],
);
const getItemString = useCallback(
(
_nodeType: string,
nodeData: unknown,
itemType: React.ReactNode,
itemString: string,
keyPath: KeyPath,
): // eslint-disable-next-line max-params
React.ReactNode => {
const forwardPath = keyPathToForward(keyPath);
return renderWithActions({
content: (
<>
{itemType} {itemString}
</>
),
fieldKey: keyPathToDisplayString(keyPath),
fieldKeyPath: forwardPath,
value: nodeData,
isNested: true,
});
},
[renderWithActions],
);
const valueRenderer = useCallback(
(
valueAsString: unknown,
value: unknown,
...keyPath: KeyPath
): React.ReactNode => {
const forwardPath = keyPathToForward(keyPath);
return renderWithActions({
content: String(valueAsString),
fieldKey: keyPathToDisplayString(keyPath),
fieldKeyPath: forwardPath,
value,
isNested: typeof value === 'object' && value !== null,
});
},
[renderWithActions],
);
const pinnedLabelRenderer = useCallback(
(keyPath: KeyPath): React.ReactNode => {
const displayKey = String(keyPath[0]);
const entry = pinnedEntries.find((e) => e.displayKey === displayKey);
return (
<span className="pretty-view__pinned-label">
<Pin
size={12}
className="pretty-view__pinned-icon"
onClick={(): void => {
if (entry) {
togglePin(entry.forwardPath);
}
}}
/>
<span>{displayKey}</span>
</span>
);
},
[togglePin, pinnedEntries],
);
return (
<div className="pretty-view">
{searchable && (
<Input
className="pretty-view__search-input"
type="text"
placeholder="Search for a field..."
value={searchQuery}
onChange={(e): void => setSearchQuery(e.target.value)}
/>
)}
{showPinned && Object.keys(filteredPinnedData).length > 0 && (
<div className="pretty-view__pinned">
<div className="pretty-view__pinned-header">PINNED ITEMS</div>
<JSONTree
key={`pinned-${searchQuery}`}
data={filteredPinnedData}
theme={theme}
invertTheme={false}
hideRoot
shouldExpandNodeInitially={shouldExpandNodeInitially}
valueRenderer={valueRenderer}
getItemString={getItemString}
labelRenderer={pinnedLabelRenderer}
/>
</div>
)}
<JSONTree
key={searchQuery}
data={filteredData}
theme={theme}
invertTheme={false}
hideRoot
shouldExpandNodeInitially={shouldExpandNodeInitially}
valueRenderer={valueRenderer}
getItemString={getItemString}
/>
</div>
);
}
export default PrettyView;

View File

@@ -0,0 +1,62 @@
// Dark theme — SigNoz design palette
export const darkTheme = {
scheme: 'signoz-dark',
author: 'signoz',
base00: 'transparent',
base01: '#161922',
base02: '#1d212d',
base03: '#62687C',
base04: '#ADB4C2',
base05: '#ADB4C2',
base06: '#ADB4C2',
base07: '#ADB4C2',
base08: '#EA6D71',
base09: '#7CEDBD',
base0A: '#7CEDBD',
base0B: '#ADB4C2',
base0C: '#23C4F8',
base0D: '#95ACFB',
base0E: '#95ACFB',
base0F: '#AD7F58',
};
// Light theme
export const lightTheme = {
scheme: 'signoz-light',
author: 'signoz',
base00: 'transparent',
base01: '#F9F9FB',
base02: '#EFF0F3',
base03: '#80828D',
base04: '#62636C',
base05: '#62636C',
base06: '#62636C',
base07: '#1E1F24',
base08: '#E5484D',
base09: '#168757',
base0A: '#168757',
base0B: '#62636C',
base0C: '#157594',
base0D: '#2F48A0',
base0E: '#2F48A0',
base0F: '#684C35',
};
export const themeExtension = {
value: ({
style,
}: {
style: Record<string, unknown>;
}): { style: Record<string, unknown>; className: string } => ({
style: { ...style },
className: 'pretty-view__row',
}),
nestedNode: ({
style,
}: {
style: Record<string, unknown>;
}): { style: Record<string, unknown>; className: string } => ({
style: { ...style },
className: 'pretty-view__nested-row',
}),
};

View File

@@ -0,0 +1,127 @@
import { useCallback, useMemo, useState } from 'react';
import {
deserializeKeyPath,
keyPathToDisplayString,
resolveValueByKeys,
serializeKeyPath,
} from '../utils';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyRecord = Record<string, any>;
const STORAGE_PREFIX = 'pinnedFields';
function loadFromStorage(storageKey: string): string[] {
try {
const stored = localStorage.getItem(storageKey);
return stored ? JSON.parse(stored) : [];
} catch {
return [];
}
}
function saveToStorage(storageKey: string, keys: string[]): void {
localStorage.setItem(storageKey, JSON.stringify(keys));
}
export interface PinnedEntry {
serializedKey: string;
displayKey: string;
forwardPath: (string | number)[];
value: unknown;
}
export interface UsePinnedFieldsReturn {
isPinned: (serializedKey: string) => boolean;
togglePin: (forwardPath: (string | number)[]) => void;
pinnedEntries: PinnedEntry[];
pinnedData: AnyRecord;
displayKeyToForwardPath: Record<string, (string | number)[]>;
}
function usePinnedFields(
data: AnyRecord,
drawerKey: string,
): UsePinnedFieldsReturn {
const storageKey = `${STORAGE_PREFIX}:${drawerKey}`;
// Stored as serialized keyPath arrays (JSON strings)
const [pinnedSerializedKeys, setPinnedSerializedKeys] = useState<Set<string>>(
() => new Set(loadFromStorage(storageKey)),
);
const togglePin = useCallback(
(forwardPath: (string | number)[]): void => {
const serialized = serializeKeyPath(forwardPath);
setPinnedSerializedKeys((prev) => {
const next = new Set(prev);
if (next.has(serialized)) {
next.delete(serialized);
} else {
next.add(serialized);
}
saveToStorage(storageKey, Array.from(next));
return next;
});
},
[storageKey],
);
const isPinned = useCallback(
(serializedKey: string): boolean => pinnedSerializedKeys.has(serializedKey),
[pinnedSerializedKeys],
);
const pinnedEntries = useMemo(
(): PinnedEntry[] =>
Array.from(pinnedSerializedKeys)
.map((serializedKey) => {
const forwardPath = deserializeKeyPath(serializedKey);
if (!forwardPath) {
return null;
}
return {
serializedKey,
displayKey: keyPathToDisplayString(
[...forwardPath].reverse() as readonly (string | number)[],
),
forwardPath,
value: resolveValueByKeys(data, forwardPath),
};
})
.filter(
(entry): entry is PinnedEntry =>
entry !== null && entry.value !== undefined,
),
[pinnedSerializedKeys, data],
);
// Flat object for the pinned JSONTree — use display key as the object key
const pinnedData = useMemo(
() =>
Object.fromEntries(
pinnedEntries.map((entry) => [entry.displayKey, entry.value]),
),
[pinnedEntries],
);
// Map from display key to original forward path — for unpin from pinned tree
const displayKeyToForwardPath = useMemo(
(): Record<string, (string | number)[]> =>
Object.fromEntries(
pinnedEntries.map((entry) => [entry.displayKey, entry.forwardPath]),
),
[pinnedEntries],
);
return {
isPinned,
togglePin,
pinnedEntries,
pinnedData,
displayKeyToForwardPath,
};
}
export default usePinnedFields;

View File

@@ -0,0 +1,82 @@
import { useMemo, useState } from 'react';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyRecord = Record<string, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnyValue = any;
function isLeaf(value: unknown): boolean {
return value === null || value === undefined || typeof value !== 'object';
}
function matchesQuery(text: string, lowerQuery: string): boolean {
return text.toLowerCase().includes(lowerQuery);
}
// Filter a single value (leaf, array, or object) against the query.
// Returns the filtered value or null if no match.
function filterValue(value: AnyValue, query: string): AnyValue | null {
if (isLeaf(value)) {
return matchesQuery(String(value), query.toLowerCase()) ? value : null;
}
if (Array.isArray(value)) {
return filterArray(value, query);
}
return filterTree(value, query);
}
// Recursively filter an array, keeping only elements that match
function filterArray(arr: AnyValue[], query: string): AnyValue[] | null {
const results = arr
.map((item) => filterValue(item, query))
.filter((item) => item !== null);
return results.length > 0 ? results : null;
}
// Recursively filter the data tree, keeping only branches with matching keys or values
export function filterTree(obj: AnyRecord, query: string): AnyRecord | null {
const result: AnyRecord = {};
const lowerQuery = query.toLowerCase();
let hasMatch = false;
for (const [key, value] of Object.entries(obj)) {
if (matchesQuery(key, lowerQuery)) {
result[key] = value;
hasMatch = true;
continue;
}
const filtered = filterValue(value, query);
if (filtered !== null) {
result[key] = filtered;
hasMatch = true;
}
}
return hasMatch ? result : null;
}
interface UseSearchFilterReturn {
searchQuery: string;
setSearchQuery: (query: string) => void;
filteredData: AnyRecord;
}
function useSearchFilter(data: AnyRecord): UseSearchFilterReturn {
const [searchQuery, setSearchQuery] = useState('');
const filteredData = useMemo(() => {
const trimmed = searchQuery.trim();
if (!trimmed) {
return data;
}
return filterTree(data, trimmed) || {};
}, [data, searchQuery]);
return { searchQuery, setSearchQuery, filteredData };
}
export default useSearchFilter;

View File

@@ -0,0 +1,2 @@
export type { PrettyViewProps } from './PrettyView';
export { default as PrettyView } from './PrettyView';

View File

@@ -0,0 +1,58 @@
import { toast } from '@signozhq/sonner';
export function copyToClipboard(value: unknown): void {
const text =
typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
// eslint-disable-next-line no-restricted-properties
navigator.clipboard.writeText(text);
toast.success('Copied to clipboard', {
richColors: true,
position: 'top-right',
});
}
// Resolve a value from a nested object using an array of keys (not dot-notation)
// e.g. resolveValueByKeys({ tagMap: { 'cloud.account.id': 'x' } }, ['tagMap', 'cloud.account.id']) → 'x'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function resolveValueByKeys(
data: Record<string, any>,
keys: (string | number)[],
): unknown {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return keys.reduce((obj: any, key) => obj?.[key], data);
}
// Convert react-json-tree's reversed keyPath to forward order
// e.g. ['cloud.account.id', 'tagMap'] → ['tagMap', 'cloud.account.id']
export function keyPathToForward(
keyPath: readonly (string | number)[],
): (string | number)[] {
return [...keyPath].reverse();
}
// Display-friendly string for a keyPath
// e.g. ['tagMap', 'cloud.account.id'] → 'tagMap.cloud.account.id'
export function keyPathToDisplayString(
keyPath: readonly (string | number)[],
): string {
return [...keyPath].reverse().join('.');
}
// Serialize keyPath for storage/comparison (JSON stringified array)
export function serializeKeyPath(keyPath: (string | number)[]): string {
return JSON.stringify(keyPath);
}
export function deserializeKeyPath(
serialized: string,
): (string | number)[] | null {
try {
const parsed = JSON.parse(serialized);
if (Array.isArray(parsed)) {
return parsed;
}
return null;
} catch {
return null;
}
}

View File

@@ -0,0 +1,42 @@
.resizable-box {
position: relative;
overflow: hidden;
&--disabled {
flex: 1;
min-height: 0;
}
&__content {
width: 100%;
height: 100%;
overflow: hidden;
}
&__handle {
position: absolute;
z-index: 10;
background: var(--l2-border);
&:hover,
&:active {
background: var(--primary);
}
&--vertical {
bottom: 0;
left: 0;
right: 0;
height: 4px;
cursor: row-resize;
}
&--horizontal {
right: 0;
top: 0;
bottom: 0;
width: 4px;
cursor: col-resize;
}
}
}

View File

@@ -0,0 +1,88 @@
import { useCallback, useRef, useState } from 'react';
import './ResizableBox.styles.scss';
export interface ResizableBoxProps {
children: React.ReactNode;
direction?: 'vertical' | 'horizontal';
defaultHeight?: number;
minHeight?: number;
maxHeight?: number;
defaultWidth?: number;
minWidth?: number;
maxWidth?: number;
onResize?: (size: number) => void;
disabled?: boolean;
className?: string;
}
function ResizableBox({
children,
direction = 'vertical',
defaultHeight = 200,
minHeight = 50,
maxHeight = Infinity,
defaultWidth = 200,
minWidth = 50,
maxWidth = Infinity,
onResize,
disabled = false,
className,
}: ResizableBoxProps): JSX.Element {
const isHorizontal = direction === 'horizontal';
const [size, setSize] = useState(isHorizontal ? defaultWidth : defaultHeight);
const containerRef = useRef<HTMLDivElement>(null);
const handleMouseDown = useCallback(
(e: React.MouseEvent): void => {
e.preventDefault();
const startPos = isHorizontal ? e.clientX : e.clientY;
const startSize = size;
const min = isHorizontal ? minWidth : minHeight;
const max = isHorizontal ? maxWidth : maxHeight;
const onMouseMove = (moveEvent: MouseEvent): void => {
const currentPos = isHorizontal ? moveEvent.clientX : moveEvent.clientY;
const delta = currentPos - startPos;
const newSize = Math.min(max, Math.max(min, startSize + delta));
setSize(newSize);
onResize?.(newSize);
};
const onMouseUp = (): void => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
document.body.style.cursor = isHorizontal ? 'col-resize' : 'row-resize';
document.body.style.userSelect = 'none';
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
},
[size, isHorizontal, minWidth, maxWidth, minHeight, maxHeight, onResize],
);
const containerStyle = disabled
? undefined
: isHorizontal
? { width: size }
: { height: size };
const handleClass = `resizable-box__handle resizable-box__handle--${direction}`;
return (
<div
ref={containerRef}
className={`resizable-box ${disabled ? 'resizable-box--disabled' : ''} ${
className || ''
}`}
style={containerStyle}
>
<div className="resizable-box__content">{children}</div>
{!disabled && <div className={handleClass} onMouseDown={handleMouseDown} />}
</div>
);
}
export default ResizableBox;

View File

@@ -0,0 +1,2 @@
export type { ResizableBoxProps } from './ResizableBox';
export { default as ResizableBox } from './ResizableBox';

View File

@@ -6538,6 +6538,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.0.tgz#d774355e41f372d5350a4d0714abb48194a489c3"
integrity sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==
"@types/lodash@^4.17.0", "@types/lodash@^4.17.15":
version "4.17.24"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.24.tgz#4ae334fc62c0e915ca8ed8e35dcc6d4eeb29215f"
integrity sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==
"@types/mdast@^3.0.0":
version "3.0.12"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.12.tgz#beeb511b977c875a5b0cc92eab6fcac2f0895514"
@@ -8952,7 +8957,7 @@ color-string@^1.9.0:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
color@^4.2.1:
color@^4.2.1, color@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/color/-/color-4.2.3.tgz"
integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
@@ -9384,6 +9389,11 @@ csstype@^3.1.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
csstype@^3.1.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
"d3-array@1 - 3", "d3-array@2 - 3", "d3-array@2.10.0 - 3":
version "3.2.3"
resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.3.tgz"
@@ -16819,6 +16829,11 @@ rc-virtual-list@^3.11.1, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2:
rc-resize-observer "^1.0.0"
rc-util "^5.36.0"
re-resizable@^6.11.2:
version "6.11.2"
resolved "https://registry.yarnpkg.com/re-resizable/-/re-resizable-6.11.2.tgz#2e8f7119ca3881d5b5aea0ffa014a80e5c1252b3"
integrity sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==
react-addons-update@15.6.3:
version "15.6.3"
resolved "https://registry.yarnpkg.com/react-addons-update/-/react-addons-update-15.6.3.tgz#c449c309154024d04087b206d0400e020547b313"
@@ -16826,6 +16841,16 @@ react-addons-update@15.6.3:
dependencies:
object-assign "^4.1.0"
react-base16-styling@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.10.0.tgz#5d5f019bd4dc5870c3e92fd9d5410533a0bbb0c6"
integrity sha512-H1k2eFB6M45OaiRru3PBXkuCcn2qNmx+gzLb4a9IPMR7tMH8oBRXU5jGbPDYG1Hz+82d88ED0vjR8BmqU3pQdg==
dependencies:
"@types/lodash" "^4.17.0"
color "^4.2.3"
csstype "^3.1.3"
lodash-es "^4.17.21"
react-beautiful-dnd@13.1.1:
version "13.1.1"
resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2"
@@ -16890,6 +16915,14 @@ react-draggable@^4.0.0, react-draggable@^4.0.3:
clsx "^1.1.1"
prop-types "^15.8.1"
react-draggable@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.5.0.tgz#0b274ccb6965fcf97ed38fcf7e3cc223bc48cdf5"
integrity sha512-VC+HBLEZ0XJxnOxVAZsdRi8rD04Iz3SiiKOoYzamjylUcju/hP9np/aZdLHf/7WOD268WMoNJMvYfB5yAK45cw==
dependencies:
clsx "^2.1.1"
prop-types "^15.8.1"
react-error-boundary@4.0.11:
version "4.0.11"
resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.11.tgz#36bf44de7746714725a814630282fee83a7c9a1c"
@@ -16980,6 +17013,14 @@ react-is@^18.3.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
react-json-tree@0.20.0:
version "0.20.0"
resolved "https://registry.yarnpkg.com/react-json-tree/-/react-json-tree-0.20.0.tgz#1e7fd26f093bd49d733f80dd9b6d40142e09f7c9"
integrity sha512-h+f9fUNAxzBx1rbrgUF7+zSWKGHDtt2VPYLErIuB0JyKGnWgFMM21ksqQyb3EXwXNnoMW2rdE5kuAaubgGOx2Q==
dependencies:
"@types/lodash" "^4.17.15"
react-base16-styling "^0.10.0"
react-kapsule@^2.5:
version "2.5.7"
resolved "https://registry.yarnpkg.com/react-kapsule/-/react-kapsule-2.5.7.tgz#dcd957ae8e897ff48055fc8ff48ed04ebe3c5bd2"
@@ -17099,6 +17140,15 @@ react-resizable@^3.0.4:
prop-types "15.x"
react-draggable "^4.0.3"
react-rnd@10.5.3:
version "10.5.3"
resolved "https://registry.yarnpkg.com/react-rnd/-/react-rnd-10.5.3.tgz#f8c484034276a561e8aa2fbf6a94580596621013"
integrity sha512-s/sIT3pGZnQ+57egijkTp9mizjIWrJz68Pq6yd+F/wniFY3IriML18dUXnQe/HP9uMiJ+9MAp44hljG99fZu6Q==
dependencies:
re-resizable "^6.11.2"
react-draggable "^4.5.0"
tslib "2.6.2"
react-router-dom-v5-compat@6.27.0:
version "6.27.0"
resolved "https://registry.yarnpkg.com/react-router-dom-v5-compat/-/react-router-dom-v5-compat-6.27.0.tgz#19429aefa130ee151e6f4487d073d919ec22d458"
@@ -19184,6 +19234,11 @@ tsconfig-paths@^4.2.0:
minimist "^1.2.6"
strip-bom "^3.0.0"
tslib@2.6.2, tslib@^2.0.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
tslib@2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
@@ -19194,11 +19249,6 @@ tslib@^1.14.1, tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0:
version "2.5.0"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz"

View File

@@ -1,115 +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/aio11ypricingruletypes"
"github.com/gorilla/mux"
)
func (provider *provider) addAIO11yRoutes(router *mux.Router) error {
if err := router.Handle("/api/v1/ai-o11y/pricing/rules", handler.New(
provider.authZ.ViewAccess(provider.aiO11yPricingRuleHandler.List),
handler.OpenAPIDef{
ID: "ListPricingRules",
Tags: []string{"ai-o11y"},
Summary: "List pricing rules",
Description: "Returns all LLM pricing rules for the authenticated org, with pagination.",
Request: nil,
RequestContentType: "",
RequestQuery: new(aio11ypricingruletypes.ListPricingRulesQuery),
Response: new(aio11ypricingruletypes.ListPricingRulesResponse),
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/ai-o11y/pricing/rules", handler.New(
provider.authZ.AdminAccess(provider.aiO11yPricingRuleHandler.Create),
handler.OpenAPIDef{
ID: "CreatePricingRule",
Tags: []string{"ai-o11y"},
Summary: "Create a pricing rule",
Description: "Creates a new LLM pricing rule for the org. Always sets is_override = true.",
Request: new(aio11ypricingruletypes.PostablePricingRule),
RequestContentType: "application/json",
Response: new(aio11ypricingruletypes.GettablePricingRule),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusConflict},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
)).Methods(http.MethodPost).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/ai-o11y/pricing/rules/{id}", handler.New(
provider.authZ.ViewAccess(provider.aiO11yPricingRuleHandler.Get),
handler.OpenAPIDef{
ID: "GetPricingRule",
Tags: []string{"ai-o11y"},
Summary: "Get a pricing rule",
Description: "Returns a single LLM pricing rule by ID.",
Request: nil,
RequestContentType: "",
Response: new(aio11ypricingruletypes.GettablePricingRule),
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/ai-o11y/pricing/rules/{id}", handler.New(
provider.authZ.AdminAccess(provider.aiO11yPricingRuleHandler.Update),
handler.OpenAPIDef{
ID: "UpdatePricingRule",
Tags: []string{"ai-o11y"},
Summary: "Update a pricing rule",
Description: "Partially updates an existing pricing rule. Changing any cost field sets is_override = true.",
Request: new(aio11ypricingruletypes.UpdatablePricingRule),
RequestContentType: "application/json",
Response: new(aio11ypricingruletypes.GettablePricingRule),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
},
)).Methods(http.MethodPut).GetError(); err != nil {
return err
}
if err := router.Handle("/api/v1/ai-o11y/pricing/rules/{id}", handler.New(
provider.authZ.AdminAccess(provider.aiO11yPricingRuleHandler.Delete),
handler.OpenAPIDef{
ID: "DeletePricingRule",
Tags: []string{"ai-o11y"},
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
}

View File

@@ -41,7 +41,7 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
RequestContentType: "application/json",
Response: new(citypes.GettableAccountWithConnectionArtifact),
ResponseContentType: "application/json",
SuccessStatusCode: http.StatusCreated,
SuccessStatusCode: http.StatusOK,
ErrorStatusCodes: []int{},
Deprecated: false,
SecuritySchemes: newSecuritySchemes(types.RoleAdmin),
@@ -138,7 +138,6 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
Summary: "List services metadata",
Description: "This endpoint lists the services metadata for the specified cloud provider",
Request: nil,
RequestQuery: new(citypes.ListServicesMetadataParams),
RequestContentType: "",
Response: new(citypes.GettableServicesMetadata),
ResponseContentType: "application/json",
@@ -159,7 +158,6 @@ func (provider *provider) addCloudIntegrationRoutes(router *mux.Router) error {
Summary: "Get service",
Description: "This endpoint gets a service for the specified cloud provider",
Request: nil,
RequestQuery: new(citypes.GetServiceParams),
RequestContentType: "",
Response: new(citypes.Service),
ResponseContentType: "application/json",

View File

@@ -11,7 +11,6 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/http/middleware"
"github.com/SigNoz/signoz/pkg/modules/aio11ypricingrule"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
@@ -33,32 +32,31 @@ import (
)
type provider struct {
config apiserver.Config
settings factory.ScopedProviderSettings
router *mux.Router
authZ *middleware.AuthZ
orgHandler organization.Handler
userHandler user.Handler
sessionHandler session.Handler
authDomainHandler authdomain.Handler
preferenceHandler preference.Handler
globalHandler global.Handler
promoteHandler promote.Handler
flaggerHandler flagger.Handler
dashboardModule dashboard.Module
dashboardHandler dashboard.Handler
metricsExplorerHandler metricsexplorer.Handler
gatewayHandler gateway.Handler
fieldsHandler fields.Handler
authzHandler authz.Handler
rawDataExportHandler rawdataexport.Handler
zeusHandler zeus.Handler
querierHandler querier.Handler
serviceAccountHandler serviceaccount.Handler
factoryHandler factory.Handler
cloudIntegrationHandler cloudintegration.Handler
ruleStateHistoryHandler rulestatehistory.Handler
aiO11yPricingRuleHandler aio11ypricingrule.Handler
config apiserver.Config
settings factory.ScopedProviderSettings
router *mux.Router
authZ *middleware.AuthZ
orgHandler organization.Handler
userHandler user.Handler
sessionHandler session.Handler
authDomainHandler authdomain.Handler
preferenceHandler preference.Handler
globalHandler global.Handler
promoteHandler promote.Handler
flaggerHandler flagger.Handler
dashboardModule dashboard.Module
dashboardHandler dashboard.Handler
metricsExplorerHandler metricsexplorer.Handler
gatewayHandler gateway.Handler
fieldsHandler fields.Handler
authzHandler authz.Handler
rawDataExportHandler rawdataexport.Handler
zeusHandler zeus.Handler
querierHandler querier.Handler
serviceAccountHandler serviceaccount.Handler
factoryHandler factory.Handler
cloudIntegrationHandler cloudintegration.Handler
ruleStateHistoryHandler rulestatehistory.Handler
}
func NewFactory(
@@ -85,7 +83,6 @@ func NewFactory(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
aiO11yPricingRuleHandler aio11ypricingrule.Handler,
) factory.ProviderFactory[apiserver.APIServer, apiserver.Config] {
return factory.NewProviderFactory(factory.MustNewName("signoz"), func(ctx context.Context, providerSettings factory.ProviderSettings, config apiserver.Config) (apiserver.APIServer, error) {
return newProvider(
@@ -115,7 +112,6 @@ func NewFactory(
factoryHandler,
cloudIntegrationHandler,
ruleStateHistoryHandler,
aiO11yPricingRuleHandler,
)
})
}
@@ -147,37 +143,35 @@ func newProvider(
factoryHandler factory.Handler,
cloudIntegrationHandler cloudintegration.Handler,
ruleStateHistoryHandler rulestatehistory.Handler,
aiO11yPricingRuleHandler aio11ypricingrule.Handler,
) (apiserver.APIServer, error) {
settings := factory.NewScopedProviderSettings(providerSettings, "github.com/SigNoz/signoz/pkg/apiserver/signozapiserver")
router := mux.NewRouter().UseEncodedPath()
provider := &provider{
config: config,
settings: settings,
router: router,
orgHandler: orgHandler,
userHandler: userHandler,
sessionHandler: sessionHandler,
authDomainHandler: authDomainHandler,
preferenceHandler: preferenceHandler,
globalHandler: globalHandler,
promoteHandler: promoteHandler,
flaggerHandler: flaggerHandler,
dashboardModule: dashboardModule,
dashboardHandler: dashboardHandler,
metricsExplorerHandler: metricsExplorerHandler,
gatewayHandler: gatewayHandler,
fieldsHandler: fieldsHandler,
authzHandler: authzHandler,
rawDataExportHandler: rawDataExportHandler,
zeusHandler: zeusHandler,
querierHandler: querierHandler,
serviceAccountHandler: serviceAccountHandler,
factoryHandler: factoryHandler,
cloudIntegrationHandler: cloudIntegrationHandler,
ruleStateHistoryHandler: ruleStateHistoryHandler,
aiO11yPricingRuleHandler: aiO11yPricingRuleHandler,
config: config,
settings: settings,
router: router,
orgHandler: orgHandler,
userHandler: userHandler,
sessionHandler: sessionHandler,
authDomainHandler: authDomainHandler,
preferenceHandler: preferenceHandler,
globalHandler: globalHandler,
promoteHandler: promoteHandler,
flaggerHandler: flaggerHandler,
dashboardModule: dashboardModule,
dashboardHandler: dashboardHandler,
metricsExplorerHandler: metricsExplorerHandler,
gatewayHandler: gatewayHandler,
fieldsHandler: fieldsHandler,
authzHandler: authzHandler,
rawDataExportHandler: rawDataExportHandler,
zeusHandler: zeusHandler,
querierHandler: querierHandler,
serviceAccountHandler: serviceAccountHandler,
factoryHandler: factoryHandler,
cloudIntegrationHandler: cloudIntegrationHandler,
ruleStateHistoryHandler: ruleStateHistoryHandler,
}
provider.authZ = middleware.NewAuthZ(settings.Logger(), orgGetter, authz)
@@ -278,10 +272,6 @@ func (provider *provider) AddToRouter(router *mux.Router) error {
return err
}
if err := provider.addAIO11yRoutes(router); err != nil {
return err
}
return nil
}

View File

@@ -1,26 +0,0 @@
package aio11ypricingrule
import (
"context"
"net/http"
"github.com/SigNoz/signoz/pkg/types/aio11ypricingruletypes"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Module interface {
List(ctx context.Context, orgID valuer.UUID, offset, limit int) ([]*aio11ypricingruletypes.PricingRule, int, error)
Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*aio11ypricingruletypes.PricingRule, error)
Create(ctx context.Context, orgID valuer.UUID, createdBy string, req *aio11ypricingruletypes.PostablePricingRule) (*aio11ypricingruletypes.PricingRule, error)
Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, req *aio11ypricingruletypes.UpdatablePricingRule) (*aio11ypricingruletypes.PricingRule, error)
Delete(ctx context.Context, orgID valuer.UUID, 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)
Create(rw http.ResponseWriter, r *http.Request)
Update(rw http.ResponseWriter, r *http.Request)
Delete(rw http.ResponseWriter, r *http.Request)
}

View File

@@ -1,226 +0,0 @@
package implaio11ypricingrule
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/aio11ypricingrule"
"github.com/SigNoz/signoz/pkg/types/aio11ypricingruletypes"
"github.com/SigNoz/signoz/pkg/types/authtypes"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/gorilla/mux"
)
const maxLimit = 100
type handler struct {
module aio11ypricingrule.Module
providerSettings factory.ProviderSettings
}
func NewHandler(module aio11ypricingrule.Module, providerSettings factory.ProviderSettings) aio11ypricingrule.Handler {
return &handler{module: module, providerSettings: providerSettings}
}
// List handles GET /api/v1/ai-o11y/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, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
var q aio11ypricingruletypes.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, aio11ypricingruletypes.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
}
items := make([]*aio11ypricingruletypes.GettablePricingRule, len(rules))
for i, r := range rules {
items[i] = aio11ypricingruletypes.NewGettablePricingRule(r)
}
render.Success(rw, http.StatusOK, &aio11ypricingruletypes.ListPricingRulesResponse{
Items: items,
Total: total,
Offset: q.Offset,
Limit: q.Limit,
})
}
// Get handles GET /api/v1/ai-o11y/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, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
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, aio11ypricingruletypes.NewGettablePricingRule(rule))
}
// Create handles POST /api/v1/ai-o11y/pricing/rules.
func (h *handler) Create(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, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
req := new(aio11ypricingruletypes.PostablePricingRule)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
rule, err := h.module.Create(ctx, orgID, claims.Email, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusCreated, aio11ypricingruletypes.NewGettablePricingRule(rule))
}
// Update handles PUT /api/v1/ai-o11y/pricing/rules/{id}
func (h *handler) Update(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, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
id, err := ruleIDFromPath(r)
if err != nil {
render.Error(rw, err)
return
}
req := new(aio11ypricingruletypes.UpdatablePricingRule)
if err := binding.JSON.BindBody(r.Body, req); err != nil {
render.Error(rw, err)
return
}
rule, err := h.module.Update(ctx, orgID, id, claims.Email, req)
if err != nil {
render.Error(rw, err)
return
}
render.Success(rw, http.StatusOK, aio11ypricingruletypes.NewGettablePricingRule(rule))
}
// Delete handles DELETE /api/v1/ai-o11y/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, err := valuer.NewUUID(claims.OrgID)
if err != nil {
render.Error(rw, err)
return
}
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"]
if raw == "" {
return valuer.UUID{}, errors.Newf(errors.TypeInvalidInput, aio11ypricingruletypes.ErrCodePricingRuleInvalidInput, "id is missing from the path")
}
id, err := valuer.NewUUID(raw)
if err != nil {
return valuer.UUID{}, errors.Wrapf(err, errors.TypeInvalidInput, aio11ypricingruletypes.ErrCodePricingRuleInvalidInput, "id is not a valid uuid")
}
return id, nil
}

View File

@@ -10,7 +10,6 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/global/signozglobal"
"github.com/SigNoz/signoz/pkg/licensing"
"github.com/SigNoz/signoz/pkg/modules/aio11ypricingrule"
"github.com/SigNoz/signoz/pkg/modules/apdex"
"github.com/SigNoz/signoz/pkg/modules/apdex/implapdex"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
@@ -43,27 +42,26 @@ import (
)
type Handlers struct {
SavedView savedview.Handler
Apdex apdex.Handler
Dashboard dashboard.Handler
QuickFilter quickfilter.Handler
TraceFunnel tracefunnel.Handler
RawDataExport rawdataexport.Handler
SpanPercentile spanpercentile.Handler
Services services.Handler
MetricsExplorer metricsexplorer.Handler
Global global.Handler
FlaggerHandler flagger.Handler
GatewayHandler gateway.Handler
Fields fields.Handler
AuthzHandler authz.Handler
ZeusHandler zeus.Handler
QuerierHandler querier.Handler
ServiceAccountHandler serviceaccount.Handler
RegistryHandler factory.Handler
CloudIntegrationHandler cloudintegration.Handler
RuleStateHistory rulestatehistory.Handler
AIO11yPricingRuleHandler aio11ypricingrule.Handler
SavedView savedview.Handler
Apdex apdex.Handler
Dashboard dashboard.Handler
QuickFilter quickfilter.Handler
TraceFunnel tracefunnel.Handler
RawDataExport rawdataexport.Handler
SpanPercentile spanpercentile.Handler
Services services.Handler
MetricsExplorer metricsexplorer.Handler
Global global.Handler
FlaggerHandler flagger.Handler
GatewayHandler gateway.Handler
Fields fields.Handler
AuthzHandler authz.Handler
ZeusHandler zeus.Handler
QuerierHandler querier.Handler
ServiceAccountHandler serviceaccount.Handler
RegistryHandler factory.Handler
CloudIntegrationHandler cloudintegration.Handler
RuleStateHistory rulestatehistory.Handler
}
func NewHandlers(

View File

@@ -16,7 +16,6 @@ import (
"github.com/SigNoz/signoz/pkg/global"
"github.com/SigNoz/signoz/pkg/http/handler"
"github.com/SigNoz/signoz/pkg/instrumentation"
"github.com/SigNoz/signoz/pkg/modules/aio11ypricingrule"
"github.com/SigNoz/signoz/pkg/modules/authdomain"
"github.com/SigNoz/signoz/pkg/modules/cloudintegration"
"github.com/SigNoz/signoz/pkg/modules/dashboard"
@@ -70,7 +69,6 @@ func NewOpenAPI(ctx context.Context, instrumentation instrumentation.Instrumenta
struct{ factory.Handler }{},
struct{ cloudintegration.Handler }{},
struct{ rulestatehistory.Handler }{},
struct{ aio11ypricingrule.Handler }{},
).New(ctx, instrumentation.ToProviderSettings(), apiserver.Config{})
if err != nil {
return nil, err

View File

@@ -3,6 +3,8 @@ package signoz
import (
"github.com/SigNoz/signoz/pkg/alertmanager"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/auditor/noopauditor"
"github.com/SigNoz/signoz/pkg/alertmanager/nfmanager/rulebasednotification"
"github.com/SigNoz/signoz/pkg/alertmanager/signozalertmanager"
"github.com/SigNoz/signoz/pkg/analytics"
@@ -10,8 +12,6 @@ import (
"github.com/SigNoz/signoz/pkg/analytics/segmentanalytics"
"github.com/SigNoz/signoz/pkg/apiserver"
"github.com/SigNoz/signoz/pkg/apiserver/signozapiserver"
"github.com/SigNoz/signoz/pkg/auditor"
"github.com/SigNoz/signoz/pkg/auditor/noopauditor"
"github.com/SigNoz/signoz/pkg/authz"
"github.com/SigNoz/signoz/pkg/cache"
"github.com/SigNoz/signoz/pkg/cache/memorycache"
@@ -288,7 +288,6 @@ func NewAPIServerProviderFactories(orgGetter organization.Getter, authz authz.Au
handlers.RegistryHandler,
handlers.CloudIntegrationHandler,
handlers.RuleStateHistory,
handlers.AIO11yPricingRuleHandler,
),
)
}

View File

@@ -1,9 +0,0 @@
package aio11ypricingruletypes
import "github.com/SigNoz/signoz/pkg/errors"
var (
ErrCodePricingRuleNotFound = errors.MustNewCode("pricing_rule_not_found")
ErrCodePricingRuleAlreadyExists = errors.MustNewCode("pricing_rule_already_exists")
ErrCodePricingRuleInvalidInput = errors.MustNewCode("pricing_rule_invalid_input")
)

View File

@@ -1,91 +0,0 @@
package aio11ypricingruletypes
import "time"
// GettablePricingRule is the HTTP response representation of a pricing rule.
type GettablePricingRule struct {
ID string `json:"id" required:"true"`
Model string `json:"model_name" required:"true"`
ModelPattern []string `json:"model_pattern" required:"true"`
Unit Unit `json:"unit" required:"true"`
CacheMode CacheMode `json:"cache_mode" required:"true"`
CostInput float64 `json:"cost_input" required:"true"`
CostOutput float64 `json:"cost_output" required:"true"`
CostCacheRead float64 `json:"cost_cache_read" required:"true"`
CostCacheWrite float64 `json:"cost_cache_write" required:"true"`
IsOverride bool `json:"is_override" required:"true"`
SyncedAt *time.Time `json:"synced_at,omitempty"`
Enabled bool `json:"enabled" required:"true"`
CreatedAt time.Time `json:"created_at" required:"true"`
UpdatedAt time.Time `json:"updated_at" required:"true"`
CreatedBy string `json:"created_by" required:"true"`
UpdatedBy string `json:"updated_by" required:"true"`
}
// NewGettablePricingRule converts a domain PricingRule to a GettablePricingRule.
func NewGettablePricingRule(r *PricingRule) *GettablePricingRule {
pattern := make([]string, len(r.ModelPattern))
copy(pattern, r.ModelPattern)
return &GettablePricingRule{
ID: r.ID,
Model: r.Model,
ModelPattern: pattern,
Unit: r.Unit,
CacheMode: r.CacheMode,
CostInput: r.CostInput,
CostOutput: r.CostOutput,
CostCacheRead: r.CostCacheRead,
CostCacheWrite: r.CostCacheWrite,
IsOverride: r.IsOverride,
SyncedAt: r.SyncedAt,
Enabled: r.Enabled,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
CreatedBy: r.CreatedBy,
UpdatedBy: r.UpdatedBy,
}
}
// PostablePricingRule is the HTTP request body for creating a pricing rule.
// All fields are required.
type PostablePricingRule struct {
Model string `json:"model_name" required:"true"`
ModelPattern []string `json:"model_pattern" required:"true"`
Unit Unit `json:"unit" required:"true"`
CacheMode CacheMode `json:"cache_mode" required:"true"`
CostInput float64 `json:"cost_input" required:"true"`
CostOutput float64 `json:"cost_output" required:"true"`
CostCacheRead float64 `json:"cost_cache_read" required:"true"`
CostCacheWrite float64 `json:"cost_cache_write" required:"true"`
Enabled bool `json:"enabled" required:"true"`
}
// UpdatablePricingRule is the HTTP request body for updating a pricing rule.
// All fields are optional; only non-nil fields are applied.
type UpdatablePricingRule struct {
Model *string `json:"model_name,omitempty"`
ModelPattern []string `json:"model_pattern,omitempty"`
Unit *Unit `json:"unit,omitempty"`
CacheMode *CacheMode `json:"cache_mode,omitempty"`
CostInput *float64 `json:"cost_input,omitempty"`
CostOutput *float64 `json:"cost_output,omitempty"`
CostCacheRead *float64 `json:"cost_cache_read,omitempty"`
CostCacheWrite *float64 `json:"cost_cache_write,omitempty"`
IsOverride *bool `json:"is_override,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}
// ListPricingRulesQuery holds the pagination parameters for listing pricing rules.
type ListPricingRulesQuery struct {
Offset int `query:"offset" json:"offset"`
Limit int `query:"limit" json:"limit"`
}
// ListPricingRulesResponse is the paginated response for listing pricing rules.
type ListPricingRulesResponse struct {
Items []*GettablePricingRule `json:"items" required:"true" nullable:"true"`
Total int `json:"total" required:"true"`
Offset int `json:"offset" required:"true"`
Limit int `json:"limit" required:"true"`
}

View File

@@ -1,80 +0,0 @@
package aio11ypricingruletypes
import (
"time"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Unit string
const (
UnitPerMillionTokens Unit = "per_million_tokens"
)
func (Unit) Enum() []any {
return []any{UnitPerMillionTokens}
}
type CacheMode string
const (
// CacheModeSubtract: cached tokens are inside input_tokens (OpenAI-style).
CacheModeSubtract CacheMode = "subtract"
// CacheModeAdditive: cached tokens are reported separately (Anthropic-style).
CacheModeAdditive CacheMode = "additive"
// CacheModeUnknown: provider behaviour is unknown; falls back to subtract.
CacheModeUnknown CacheMode = "unknown"
)
func (CacheMode) Enum() []any {
return []any{CacheModeSubtract, CacheModeAdditive, CacheModeUnknown}
}
// PricingRule is the domain model for an LLM pricing rule.
// It has no serialisation concerns — use GettablePricingRule for HTTP responses.
type PricingRule struct {
types.TimeAuditable
types.UserAuditable
ID string
OrgID valuer.UUID
Model string
ModelPattern []string
Unit Unit
CacheMode CacheMode
CostInput float64
CostOutput float64
CostCacheRead float64
CostCacheWrite float64
// IsOverride marks that cost fields were manually edited.
// When true the sync job skips this rule.
IsOverride bool
SyncedAt *time.Time
Enabled bool
}
// NewPricingRuleFromStorable converts a StorablePricingRule to a PricingRule.
func NewPricingRuleFromStorable(s *StorablePricingRule) *PricingRule {
pattern := make([]string, len(s.ModelPattern))
copy(pattern, s.ModelPattern)
return &PricingRule{
TimeAuditable: s.TimeAuditable,
UserAuditable: s.UserAuditable,
ID: s.ID.StringValue(),
OrgID: s.OrgID,
Model: s.Model,
ModelPattern: pattern,
Unit: s.Unit,
CacheMode: s.CacheMode,
CostInput: s.CostInput,
CostOutput: s.CostOutput,
CostCacheRead: s.CostCacheRead,
CostCacheWrite: s.CostCacheWrite,
IsOverride: s.IsOverride,
SyncedAt: s.SyncedAt,
Enabled: s.Enabled,
}
}

View File

@@ -1,70 +0,0 @@
package aio11ypricingruletypes
import (
"database/sql/driver"
"encoding/json"
"fmt"
"time"
"github.com/SigNoz/signoz/pkg/types"
"github.com/SigNoz/signoz/pkg/valuer"
"github.com/uptrace/bun"
)
// StringSlice is a []string that is stored as a JSON text column.
// It is compatible with both SQLite and PostgreSQL.
type StringSlice []string
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 fmt.Errorf("aio11ypricingruletypes: cannot scan %T into StringSlice", src)
}
return json.Unmarshal(raw, s)
}
// StorablePricingRule is the bun/DB representation of an LLM pricing rule.
type StorablePricingRule struct {
bun.BaseModel `bun:"table:llm_pricing_rules,alias:llm_pricing_rules"`
types.Identifiable
types.TimeAuditable
types.UserAuditable
OrgID valuer.UUID `bun:"org_id,type:text,notnull"`
Model string `bun:"model,type:text,notnull"`
ModelPattern StringSlice `bun:"model_pattern,type:text,notnull"`
Unit Unit `bun:"unit,type:text,notnull"`
CacheMode CacheMode `bun:"cache_mode,type:text,notnull"`
CostInput float64 `bun:"cost_input,notnull"`
CostOutput float64 `bun:"cost_output,notnull"`
CostCacheRead float64 `bun:"cost_cache_read,notnull"`
CostCacheWrite float64 `bun:"cost_cache_write,notnull"`
// IsOverride marks that cost fields were manually edited.
// When true the sync job skips this rule.
IsOverride bool `bun:"is_override,notnull,default:false"`
// SourceConfig holds the last upstream pricing snapshot; used to revert
// when IsOverride is cleared.
SourceConfig string `bun:"source_config,type:text,notnull,default:'{}'"`
SyncedAt *time.Time `bun:"synced_at"`
Enabled bool `bun:"enabled,notnull,default:true"`
}

View File

@@ -1,19 +0,0 @@
package aio11ypricingruletypes
import (
"context"
"github.com/SigNoz/signoz/pkg/valuer"
)
type Store interface {
List(ctx context.Context, orgID valuer.UUID, offset, limit int) ([]*StorablePricingRule, int, error)
Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*StorablePricingRule, error)
Create(ctx context.Context, rule *StorablePricingRule) error
Update(ctx context.Context, rule *StorablePricingRule) error
Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error
}

View File

@@ -62,10 +62,6 @@ type GettableServicesMetadata struct {
Services []*ServiceMetadata `json:"services" required:"true" nullable:"false"`
}
type ListServicesMetadataParams struct {
CloudIntegrationID valuer.UUID `query:"cloud_integration_id" required:"false"`
}
// Service represents a cloud integration service with its definition,
// cloud integration service is non nil only when the service entry exists in DB with ANY config (enabled or disabled).
type Service struct {
@@ -73,10 +69,6 @@ type Service struct {
CloudIntegrationService *CloudIntegrationService `json:"cloudIntegrationService" required:"true" nullable:"true"`
}
type GetServiceParams struct {
CloudIntegrationID valuer.UUID `query:"cloud_integration_id" required:"false"`
}
type UpdatableService struct {
Config *ServiceConfig `json:"config" required:"true" nullable:"false"`
}