Compare commits

..

4 Commits

Author SHA1 Message Date
Vikrant Gupta
e8324581b3 fix(tokenizer): accept the previous token pair only within the rotation duration (#12941)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
cacheci / tests (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run
#### Description

- `Token.Rotate` accepted the previous token pair only when the rotation
was older than `rotation.duration` and rejected it inside the window,
the inverse of the documented intent.
- With the opaque tokenizer, two holders of the same pair (browser tabs,
the axios interceptor and the SSE wrapper) racing at the rotation
boundary meant the loser got 401 on `/sessions/rotate` and the frontend
logged the user out. It also left a stale pair exchangeable for the live
session until the next rotation.
- `RotateToken` now detects that `Rotate` left the stored row untouched
and returns the current pair without rewriting it; the previous check
compared against the input and could never match.
- Adds a unit test for the previous-pair path inside and outside the
window.

#### Additional Information

The JWT tokenizer is unaffected since its rotation is stateless.
2026-09-22 09:58:52 +00:00
Naman Verma
13a57ebb9c chore: remove v1 dashboards code from backend (#12932)
<!--Reference issues using `Closes #issue-number` to enable automatic
closure on merge. -->
#### Issues closed by this PR

Closes https://github.com/SigNoz/pulse-pod/issues/321
2026-09-22 08:46:28 +00:00
Aditya Singh
d39467f5ef chore(logs): delete the old logs explorer (#12930)
#### Description

- `/logs/old-logs-explorer` has been rendering nothing since #8299
gutted its two endpoints. deleted the page along with its containers,
the logs redux slice, the legacy `api/logs` clients and the logql
parser. `LogViewMode` and the restricted field constants move out first
since they have consumers elsewhere.
- deleted what the removal orphans.. the logql test fixture,
`CategoryHeading` and the antd table behind `components/Logs/TableView`.
that folder does not go entirely, `useLogsTableColumns` and
`ColumnTypeRender` still have consumers.
- `ROUTES.LOGS` and `ROUTES.LOGS_EXPLORER` are the same path and both
were registered exact. Switch takes the first so the second entry never
ran and its chunk just duplicated the first. dropped the entry, both
constants stay.

#### Issues closed by this PR

Part of https://github.com/SigNoz/engineering-pod/issues/6103

#### Additional Information

- frontend only. `GET /api/v1/logs` and `/api/v1/logs/aggregate` stay
registered and still return empty payloads, so this can go in without a
backend change.
- overlaps #12919 (old trace explorer) on the `NewExplorerCTA` cleanup,
and overlaps the log details v1 removal on a few files it deletes
outright. whichever merges second needs a rebase.
- cc. @therealpandey
2026-09-22 04:44:25 +00:00
Gaurav Tewari
d457ce6144 feat(llm-observability): move module tabs to RouteTab , add tab icons & other ui changes (#12762)
#### Description

- Replaces the bespoke LLM Observability tab shell
(`container/LLMObservability/LLMObservability.tsx` +
`useLLMObservabilityTabs`) with the shared `RouteTab` component that
every other module page already uses. Tabs are now declared as
`TabRoutes` in `pages/LLMObservability/constants.tsx`.
- Adds icons to the tab labels across LLM Observability, Infrastructure
Monitoring, All Errors, Messaging Queues, Meter Explorer and Logs
Settings, following the existing `.tab-item` pattern.
- Fixes tab-panel padding and aligns the source-filter select height
with the adjacent search input.
- Removed the dashboardInfoWithActions


#### Additional Information

`AllErrors/config.ts` is renamed to `.tsx` since the tab labels now
contain JSX.

Visual Diff - 
1. Look into the wrapper  - 
 a. Before 
<img width="1474" height="997" alt="image"
src="https://github.com/user-attachments/assets/39fe854b-fa79-4e24-9227-4da77825f726"
/>
b. Now 
<img width="1489" height="991" alt="image"
src="https://github.com/user-attachments/assets/4c02e991-3764-4f66-9efc-bef934ca5e2a"
/>



2. Removed the dashboardInfoWithActions 
a. Before 
<img width="1392" height="304" alt="image"
src="https://github.com/user-attachments/assets/bb2efef8-5126-4728-b221-b2cc2b3da18f"
/>
b. now
<img width="1420" height="290" alt="image"
src="https://github.com/user-attachments/assets/50412047-5eff-4035-ac6c-ed79393070ac"
/>


3. After feedback from nitya we have to hide add variable and the
actions button as well.
a. Before
<img width="1368" height="597" alt="image"
src="https://github.com/user-attachments/assets/669de39b-bbc2-4b29-a16e-139457fbb483"
/>

b. Now
<img width="1382" height="728" alt="image"
src="https://github.com/user-attachments/assets/7973dc3a-9914-42ed-a334-887ea73b1ba0"
/>

---------

Co-authored-by: Gaurav Tewari <tewarig@users.noreply.github.com>
2026-09-22 04:44:00 +00:00
153 changed files with 302 additions and 7341 deletions

View File

@@ -3454,29 +3454,6 @@ components:
required:
- customValue
type: object
DashboardtypesDashboard:
properties:
createdAt:
format: date-time
type: string
createdBy:
type: string
data:
$ref: '#/components/schemas/DashboardtypesStorableDashboardData'
id:
type: string
locked:
type: boolean
org_id:
type: string
source:
$ref: '#/components/schemas/DashboardtypesSource'
updatedAt:
format: date-time
type: string
updatedBy:
type: string
type: object
DashboardtypesDashboardPanelRef:
properties:
dashboardId:
@@ -3656,13 +3633,6 @@ components:
timeRangeEnabled:
type: boolean
type: object
DashboardtypesGettablePublicDashboardData:
properties:
dashboard:
$ref: '#/components/schemas/DashboardtypesDashboard'
publicDashboard:
$ref: '#/components/schemas/DashboardtypesGettablePublicDasbhboard'
type: object
DashboardtypesGettablePublicDashboardDataV2:
properties:
dashboard:
@@ -4447,9 +4417,6 @@ components:
- normal
- percent
type: string
DashboardtypesStorableDashboardData:
additionalProperties: {}
type: object
DashboardtypesTableFormatting:
properties:
columnUnits:
@@ -7225,22 +7192,6 @@ components:
- attributes
- totalKeys
type: object
MetricsexplorertypesMetricDashboard:
properties:
dashboardId:
type: string
dashboardName:
type: string
widgetId:
type: string
widgetName:
type: string
required:
- dashboardName
- dashboardId
- widgetId
- widgetName
type: object
MetricsexplorertypesMetricDashboardPanelsResponse:
properties:
dashboards:
@@ -7251,16 +7202,6 @@ components:
required:
- dashboards
type: object
MetricsexplorertypesMetricDashboardsResponse:
properties:
dashboards:
items:
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboard'
nullable: true
type: array
required:
- dashboards
type: object
MetricsexplorertypesMetricHighlightsResponse:
properties:
activeTimeSeries:
@@ -13303,112 +13244,6 @@ paths:
summary: Update org preference
tags:
- preferences
/api/v1/public/dashboards/{id}:
get:
deprecated: false
description: This endpoint returns the sanitized dashboard data for public access
operationId: GetPublicDashboardData
parameters:
- in: path
name: id
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/DashboardtypesGettablePublicDashboardData'
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
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- anonymous:
- public-dashboard:read
summary: Get public dashboard data
tags:
- dashboard
/api/v1/public/dashboards/{id}/widgets/{idx}/query_range:
get:
deprecated: false
description: This endpoint return query range results for a widget of public
dashboard
operationId: GetPublicDashboardWidgetQueryRange
parameters:
- in: path
name: id
required: true
schema:
type: string
- in: path
name: idx
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/Querybuildertypesv5QueryRangeResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"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:
- anonymous:
- public-dashboard:read
summary: Get query range result
tags:
- dashboard
/api/v1/roles:
get:
deprecated: false
@@ -19660,74 +19495,6 @@ paths:
summary: Get metric attributes
tags:
- metrics
/api/v2/metrics/dashboards:
get:
deprecated: false
description: This endpoint returns associated dashboards for a specified metric
operationId: GetMetricDashboards
parameters:
- description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
in: query
name: metricName
required: true
schema:
description: The name of the metric. May contain slashes (e.g. cloud-provider
metrics like run.googleapis.com/request_latencies).
type: string
responses:
"200":
content:
application/json:
schema:
properties:
data:
$ref: '#/components/schemas/MetricsexplorertypesMetricDashboardsResponse'
status:
type: string
required:
- status
- data
type: object
description: OK
"400":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Bad Request
"401":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"404":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Not Found
"500":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Internal Server Error
security:
- api_key:
- VIEWER
- tokenizer:
- VIEWER
summary: Get metric dashboards
tags:
- metrics
/api/v2/metrics/highlights:
get:
deprecated: false

View File

@@ -52,7 +52,7 @@ func (module *module) CreatePublic(ctx context.Context, orgID valuer.UUID, publi
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
dashboard, err := module.GetV2(ctx, orgID, publicDashboard.DashboardID)
if err != nil {
return err
}
@@ -90,15 +90,6 @@ func (module *module) GetPublic(ctx context.Context, orgID valuer.UUID, dashboar
return dashboardtypes.NewPublicDashboardFromStorablePublicDashboard(storablePublicDashboard), nil
}
func (module *module) GetDashboardByPublicID(ctx context.Context, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
if err != nil {
return nil, err
}
return dashboardtypes.NewDashboardFromStorableDashboard(storableDashboard), nil
}
func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id valuer.UUID, orgs []*types.Organization) ([]coretypes.Selector, valuer.UUID, error) {
orgIDs := make([]string, len(orgs))
for idx, org := range orgs {
@@ -116,24 +107,6 @@ func (module *module) GetPublicDashboardSelectorsAndOrg(ctx context.Context, id
}, storableDashboard.OrgID, nil
}
func (module *module) GetPublicWidgetQueryRange(ctx context.Context, id valuer.UUID, widgetIdx, startTime, endTime uint64) (*querybuildertypesv5.QueryRangeResponse, error) {
ctx = ctxtypes.NewContextWithCommentVals(ctx, map[string]string{
instrumentationtypes.CodeNamespace: "dashboard",
instrumentationtypes.CodeFunctionName: "GetPublicWidgetQueryRange",
})
dashboard, err := module.GetDashboardByPublicID(ctx, id)
if err != nil {
return nil, err
}
query, err := dashboard.GetWidgetQuery(startTime, endTime, widgetIdx, module.settings.Logger())
if err != nil {
return nil, err
}
return module.querier.QueryRange(ctx, dashboard.OrgID, query)
}
func (module *module) GetDashboardByPublicIDV2(ctx context.Context, id valuer.UUID) (*dashboardtypes.DashboardV2, error) {
storableDashboard, err := module.store.GetDashboardByPublicID(ctx, id.StringValue())
if err != nil {
@@ -189,7 +162,7 @@ func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publi
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
dashboard, err := module.Get(ctx, orgID, publicDashboard.DashboardID)
dashboard, err := module.GetV2(ctx, orgID, publicDashboard.DashboardID)
if err != nil {
return err
}
@@ -200,34 +173,13 @@ func (module *module) UpdatePublic(ctx context.Context, orgID valuer.UUID, publi
return module.store.UpdatePublic(ctx, dashboardtypes.NewStorablePublicDashboardFromPublicDashboard(publicDashboard))
}
func (module *module) Delete(ctx context.Context, orgID valuer.UUID, id valuer.UUID) error {
dashboard, err := module.Get(ctx, orgID, id)
if err != nil {
return err
}
if err := dashboard.ErrIfNotDeletable(); err != nil {
return err
}
if dashboard.Locked {
return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "dashboard is locked, please unlock the dashboard to be delete it")
}
return module.delete(ctx, orgID, id)
}
func (module *module) DeleteUnsafe(ctx context.Context, orgID, id valuer.UUID) error {
return module.delete(ctx, orgID, id)
}
func (module *module) DeletePublic(ctx context.Context, orgID valuer.UUID, dashboardID valuer.UUID) error {
_, err := module.licensing.GetActive(ctx, orgID)
if err != nil {
return errors.New(errors.TypeLicenseUnavailable, errors.CodeLicenseUnavailable, "a valid license is not available").WithAdditional("this feature requires a valid license").WithAdditional(err.Error())
}
dashboard, err := module.Get(ctx, orgID, dashboardID)
dashboard, err := module.GetV2(ctx, orgID, dashboardID)
if err != nil {
return err
}
@@ -260,10 +212,6 @@ func (module *module) Collect(ctx context.Context, orgID valuer.UUID) (map[strin
return stats, nil
}
func (module *module) Create(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, data dashboardtypes.PostableDashboard) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Create(ctx, orgID, createdBy, creator, source, data)
}
func (module *module) CreateV2(ctx context.Context, orgID valuer.UUID, createdBy string, creator valuer.UUID, source dashboardtypes.Source, postable dashboardtypes.PostableDashboardV2) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.CreateV2(ctx, orgID, createdBy, creator, source, postable)
}
@@ -346,30 +294,10 @@ func (module *module) DeleteView(ctx context.Context, orgID valuer.UUID, id valu
return module.pkgDashboardModule.DeleteView(ctx, orgID, id)
}
func (module *module) Get(ctx context.Context, orgID valuer.UUID, id valuer.UUID) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Get(ctx, orgID, id)
}
func (module *module) GetByMetricNames(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]dashboardtypes.DashboardPanelRef, error) {
return module.pkgDashboardModule.GetByMetricNames(ctx, orgID, metricNames)
}
func (module *module) GetByMetricNamesV2(ctx context.Context, orgID valuer.UUID, metricNames []string) (map[string][]dashboardtypes.DashboardPanelRef, error) {
return module.pkgDashboardModule.GetByMetricNamesV2(ctx, orgID, metricNames)
}
func (module *module) List(ctx context.Context, orgID valuer.UUID) ([]*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.List(ctx, orgID)
}
func (module *module) Update(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, data dashboardtypes.UpdatableDashboard, diff int) (*dashboardtypes.Dashboard, error) {
return module.pkgDashboardModule.Update(ctx, orgID, id, updatedBy, data, diff)
}
func (module *module) LockUnlock(ctx context.Context, orgID valuer.UUID, id valuer.UUID, updatedBy string, isAdmin bool, lock bool) error {
return module.pkgDashboardModule.LockUnlock(ctx, orgID, id, updatedBy, isAdmin, lock)
}
func (module *module) ReconcileSystemDashboards(ctx context.Context, orgID valuer.UUID) error {
return module.pkgDashboardModule.ReconcileSystemDashboards(ctx, orgID)
}
@@ -377,12 +305,3 @@ func (module *module) ReconcileSystemDashboards(ctx context.Context, orgID value
func (module *module) GetSystemDashboard(ctx context.Context, orgID valuer.UUID, name string) (*dashboardtypes.DashboardV2, error) {
return module.pkgDashboardModule.GetSystemDashboard(ctx, orgID, name)
}
func (module *module) delete(ctx context.Context, orgID, id valuer.UUID) error {
return module.store.RunInTx(ctx, func(ctx context.Context) error {
if err := module.store.DeletePublic(ctx, id.String()); err != nil && !errors.Ast(err, errors.TypeNotFound) {
return err
}
return module.store.Delete(ctx, orgID, id)
})
}

View File

@@ -462,7 +462,7 @@ func (m *module) relatedAssetImpact(ctx context.Context, orgID valuer.UUID, metr
droppedSet[label] = struct{}{}
}
if dashboards, err := m.dashboard.GetByMetricNames(ctx, orgID, []string{metricName}); err != nil {
if dashboards, err := m.dashboard.GetByMetricNamesV2(ctx, orgID, []string{metricName}); err != nil {
m.logger.WarnContext(ctx, "failed to fetch related dashboards for reduction preview", slog.String("metric_name", metricName), errors.Attr(err))
} else {
for _, item := range dashboards[metricName] {

View File

@@ -61,6 +61,7 @@
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
"AI_OBSERVABILITY_EXPLORER": "SigNoz | AI Observability Explorer",
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
}

View File

@@ -42,7 +42,6 @@
"NOT_FOUND": "SigNoz | Page Not Found",
"LOGS": "SigNoz | Logs",
"LOGS_EXPLORER": "SigNoz | Logs Explorer",
"OLD_LOGS_EXPLORER": "SigNoz | Old Logs Explorer",
"LIVE_LOGS": "SigNoz | Live Logs",
"LOGS_PIPELINES": "SigNoz | Logs Pipelines",
"HOME_PAGE": "Open source Observability Platform | SigNoz",
@@ -86,6 +85,7 @@
"INTEGRATIONS_DETAIL": "SigNoz | Integration",
"PUBLIC_DASHBOARD": "SigNoz | Dashboard",
"AI_OBSERVABILITY_OVERVIEW": "SigNoz | AI Observability Overview",
"AI_OBSERVABILITY_EXPLORER": "SigNoz | AI Observability Explorer",
"AI_OBSERVABILITY_CONFIGURATION": "SigNoz | AI Observability Configuration",
"AI_OBSERVABILITY_ATTRIBUTE_MAPPING": "SigNoz | AI Observability Attribute Mapping"
}

View File

@@ -1597,10 +1597,6 @@ describe('PrivateRoute', () => {
LOGS: { path: ROUTES.LOGS, deniedRoles: DENIED_ROLES },
LOGS_EXPLORER: { path: ROUTES.LOGS_EXPLORER, deniedRoles: DENIED_ROLES },
LIVE_LOGS: { path: ROUTES.LIVE_LOGS, deniedRoles: DENIED_ROLES },
OLD_LOGS_EXPLORER: {
path: ROUTES.OLD_LOGS_EXPLORER,
deniedRoles: DENIED_ROLES,
},
METRICS_EXPLORER: {
path: ROUTES.METRICS_EXPLORER,
deniedRoles: DENIED_ROLES,

View File

@@ -154,14 +154,6 @@ export const Logs = Loadable(
() => import(/* webpackChunkName: "Logs" */ 'pages/LogsModulePage'),
);
export const LogsExplorer = Loadable(
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/LogsModulePage'),
);
export const OldLogsExplorer = Loadable(
() => import(/* webpackChunkName: "Logs Explorer" */ 'pages/Logs'),
);
export const LiveLogs = Loadable(
() => import(/* webpackChunkName: "Live Logs" */ 'pages/LiveLogs'),
);

View File

@@ -26,13 +26,11 @@ import {
LiveLogs,
Login,
Logs,
LogsExplorer,
LogsIndexToFields,
LogsSaveViews,
MessagingQueuesMainPage,
MeterExplorerPage,
MetricsExplorer,
OldLogsExplorer,
OnboardingV2,
OrgOnboarding,
PasswordReset,
@@ -284,20 +282,6 @@ const routes: AppRoutes[] = [
key: 'LOGS',
isPrivate: true,
},
{
path: ROUTES.LOGS_EXPLORER,
exact: true,
component: LogsExplorer,
key: 'LOGS_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.OLD_LOGS_EXPLORER,
exact: true,
component: OldLogsExplorer,
key: 'OLD_LOGS_EXPLORER',
isPrivate: true,
},
{
path: ROUTES.LIVE_LOGS,
exact: true,

View File

@@ -36,16 +36,12 @@ import type {
GetDashboardV2200,
GetDashboardV2PathParameters,
GetPublicDashboard200,
GetPublicDashboardData200,
GetPublicDashboardDataPathParameters,
GetPublicDashboardDataV2200,
GetPublicDashboardDataV2PathParameters,
GetPublicDashboardPanelQueryRangeV2200,
GetPublicDashboardPanelQueryRangeV2Params,
GetPublicDashboardPanelQueryRangeV2PathParameters,
GetPublicDashboardPathParameters,
GetPublicDashboardWidgetQueryRange200,
GetPublicDashboardWidgetQueryRangePathParameters,
GetSystemDashboard200,
GetSystemDashboardPathParameters,
ListDashboardViews200,
@@ -474,217 +470,6 @@ export const useUpdatePublicDashboard = <
> => {
return useMutation(getUpdatePublicDashboardMutationOptions(options));
};
/**
* This endpoint returns the sanitized dashboard data for public access
* @summary Get public dashboard data
*/
export const getPublicDashboardData = (
{ id }: GetPublicDashboardDataPathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetPublicDashboardData200>({
url: `/api/v1/public/dashboards/${id}`,
method: 'GET',
signal,
});
};
export const getGetPublicDashboardDataQueryKey = ({
id,
}: GetPublicDashboardDataPathParameters) => {
return [`/api/v1/public/dashboards/${id}`] as const;
};
export const getGetPublicDashboardDataQueryOptions = <
TData = Awaited<ReturnType<typeof getPublicDashboardData>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetPublicDashboardDataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardData>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetPublicDashboardDataQueryKey({ id });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getPublicDashboardData>>
> = ({ signal }) => getPublicDashboardData({ id }, signal);
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardData>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetPublicDashboardDataQueryResult = NonNullable<
Awaited<ReturnType<typeof getPublicDashboardData>>
>;
export type GetPublicDashboardDataQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get public dashboard data
*/
export function useGetPublicDashboardData<
TData = Awaited<ReturnType<typeof getPublicDashboardData>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id }: GetPublicDashboardDataPathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardData>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPublicDashboardDataQueryOptions({ id }, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary Get public dashboard data
*/
export const invalidateGetPublicDashboardData = async (
queryClient: QueryClient,
{ id }: GetPublicDashboardDataPathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetPublicDashboardDataQueryKey({ id }) },
options,
);
return queryClient;
};
/**
* This endpoint return query range results for a widget of public dashboard
* @summary Get query range result
*/
export const getPublicDashboardWidgetQueryRange = (
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetPublicDashboardWidgetQueryRange200>({
url: `/api/v1/public/dashboards/${id}/widgets/${idx}/query_range`,
method: 'GET',
signal,
});
};
export const getGetPublicDashboardWidgetQueryRangeQueryKey = ({
id,
idx,
}: GetPublicDashboardWidgetQueryRangePathParameters) => {
return [`/api/v1/public/dashboards/${id}/widgets/${idx}/query_range`] as const;
};
export const getGetPublicDashboardWidgetQueryRangeQueryOptions = <
TData = Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetPublicDashboardWidgetQueryRangeQueryKey({ id, idx });
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>
> = ({ signal }) => getPublicDashboardWidgetQueryRange({ id, idx }, signal);
return {
queryKey,
queryFn,
enabled: id !== null && id !== undefined && idx !== null && idx !== undefined,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetPublicDashboardWidgetQueryRangeQueryResult = NonNullable<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>
>;
export type GetPublicDashboardWidgetQueryRangeQueryError =
ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get query range result
*/
export function useGetPublicDashboardWidgetQueryRange<
TData = Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getPublicDashboardWidgetQueryRange>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetPublicDashboardWidgetQueryRangeQueryOptions(
{ id, idx },
options,
);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary Get query range result
*/
export const invalidateGetPublicDashboardWidgetQueryRange = async (
queryClient: QueryClient,
{ id, idx }: GetPublicDashboardWidgetQueryRangePathParameters,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetPublicDashboardWidgetQueryRangeQueryKey({ id, idx }) },
options,
);
return queryClient;
};
/**
* Returns every saved view in the calling user's org. Saved views are shared org-wide.
* @summary List dashboard saved views

View File

@@ -24,8 +24,6 @@ import type {
GetMetricAlertsParams,
GetMetricAttributes200,
GetMetricAttributesParams,
GetMetricDashboards200,
GetMetricDashboardsParams,
GetMetricDashboardsV2200,
GetMetricDashboardsV2Params,
GetMetricHighlights200,
@@ -1096,104 +1094,6 @@ export const invalidateGetMetricAttributes = async (
return queryClient;
};
/**
* This endpoint returns associated dashboards for a specified metric
* @summary Get metric dashboards
*/
export const getMetricDashboards = (
params: GetMetricDashboardsParams,
signal?: AbortSignal,
) => {
return GeneratedAPIInstance<GetMetricDashboards200>({
url: `/api/v2/metrics/dashboards`,
method: 'GET',
params,
signal,
});
};
export const getGetMetricDashboardsQueryKey = (
params?: GetMetricDashboardsParams,
) => {
return [`/api/v2/metrics/dashboards`, ...(params ? [params] : [])] as const;
};
export const getGetMetricDashboardsQueryOptions = <
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
TError,
TData
>;
},
) => {
const { query: queryOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ?? getGetMetricDashboardsQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getMetricDashboards>>
> = ({ signal }) => getMetricDashboards(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type GetMetricDashboardsQueryResult = NonNullable<
Awaited<ReturnType<typeof getMetricDashboards>>
>;
export type GetMetricDashboardsQueryError = ErrorType<RenderErrorResponseDTO>;
/**
* @summary Get metric dashboards
*/
export function useGetMetricDashboards<
TData = Awaited<ReturnType<typeof getMetricDashboards>>,
TError = ErrorType<RenderErrorResponseDTO>,
>(
params: GetMetricDashboardsParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<typeof getMetricDashboards>>,
TError,
TData
>;
},
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = getGetMetricDashboardsQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
return withQueryKey(query, queryOptions.queryKey);
}
/**
* @summary Get metric dashboards
*/
export const invalidateGetMetricDashboards = async (
queryClient: QueryClient,
params: GetMetricDashboardsParams,
options?: InvalidateOptions,
): Promise<QueryClient> => {
await queryClient.invalidateQueries(
{ queryKey: getGetMetricDashboardsQueryKey(params) },
options,
);
return queryClient;
};
/**
* This endpoint returns highlights like number of datapoints, totaltimeseries, active time series, last received time for a specified metric
* @summary Get metric highlights

View File

@@ -4886,50 +4886,6 @@ export interface DashboardtypesCustomVariableSpecDTO {
customValue: string;
}
export interface DashboardtypesStorableDashboardDataDTO {
[key: string]: unknown;
}
export enum DashboardtypesSourceDTO {
user = 'user',
system = 'system',
integration = 'integration',
}
export interface DashboardtypesDashboardDTO {
/**
* @type string
* @format date-time
*/
createdAt?: string;
/**
* @type string
*/
createdBy?: string;
data?: DashboardtypesStorableDashboardDataDTO;
/**
* @type string
*/
id?: string;
/**
* @type boolean
*/
locked?: boolean;
/**
* @type string
*/
org_id?: string;
source?: DashboardtypesSourceDTO;
/**
* @type string
* @format date-time
*/
updatedAt?: string;
/**
* @type string
*/
updatedBy?: string;
}
export interface DashboardtypesDashboardPanelRefDTO {
/**
* @type string
@@ -5868,6 +5824,11 @@ export interface DashboardtypesDashboardViewDTO {
updatedAt?: string;
}
export enum DashboardtypesSourceDTO {
user = 'user',
system = 'system',
integration = 'integration',
}
export interface TagtypesGettableTagDTO {
/**
* @type string
@@ -5945,11 +5906,6 @@ export interface DashboardtypesGettablePublicDasbhboardDTO {
timeRangeEnabled?: boolean;
}
export interface DashboardtypesGettablePublicDashboardDataDTO {
dashboard?: DashboardtypesDashboardDTO;
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
}
export interface DashboardtypesGettablePublicDashboardDataV2DTO {
dashboard?: DashboardtypesGettableDashboardV2DTO;
publicDashboard?: DashboardtypesGettablePublicDasbhboardDTO;
@@ -9022,25 +8978,6 @@ export interface MetricsexplorertypesMetricAttributesResponseDTO {
totalKeys: number;
}
export interface MetricsexplorertypesMetricDashboardDTO {
/**
* @type string
*/
dashboardId: string;
/**
* @type string
*/
dashboardName: string;
/**
* @type string
*/
widgetId: string;
/**
* @type string
*/
widgetName: string;
}
export interface MetricsexplorertypesMetricDashboardPanelsResponseDTO {
/**
* @type array,null
@@ -9048,13 +8985,6 @@ export interface MetricsexplorertypesMetricDashboardPanelsResponseDTO {
dashboards: DashboardtypesDashboardPanelRefDTO[] | null;
}
export interface MetricsexplorertypesMetricDashboardsResponseDTO {
/**
* @type array,null
*/
dashboards: MetricsexplorertypesMetricDashboardDTO[] | null;
}
export interface MetricsexplorertypesMetricHighlightsResponseDTO {
/**
* @type integer
@@ -12488,29 +12418,6 @@ export type GetOrgPreference200 = {
export type UpdateOrgPreferencePathParameters = {
name: string;
};
export type GetPublicDashboardDataPathParameters = {
id: string;
};
export type GetPublicDashboardData200 = {
data: DashboardtypesGettablePublicDashboardDataDTO;
/**
* @type string
*/
status: string;
};
export type GetPublicDashboardWidgetQueryRangePathParameters = {
id: string;
idx: string;
};
export type GetPublicDashboardWidgetQueryRange200 = {
data: Querybuildertypesv5QueryRangeResponseDTO;
/**
* @type string
*/
status: string;
};
export type ListRoles200 = {
/**
* @type array
@@ -13468,22 +13375,6 @@ export type GetMetricAttributes200 = {
status: string;
};
export type GetMetricDashboardsParams = {
/**
* @type string
* @description The name of the metric. May contain slashes (e.g. cloud-provider metrics like run.googleapis.com/request_latencies).
*/
metricName: string;
};
export type GetMetricDashboards200 = {
data: MetricsexplorertypesMetricDashboardsResponseDTO;
/**
* @type string
*/
status: string;
};
export type GetMetricHighlightsParams = {
/**
* @type string

View File

@@ -1,23 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/logs/addToSelectedFields';
const addToSelectedFields = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.post(`/logs/fields`, props);
return {
statusCode: 200,
error: null,
message: '',
payload: data.data,
};
} catch (error) {
return Promise.reject(ErrorResponseHandler(error as AxiosError));
}
};
export default addToSelectedFields;

View File

@@ -1,26 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/logs/getLogs';
const GetLogs = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.get(`/logs`, {
params: props,
});
return {
statusCode: 200,
error: null,
message: '',
payload: data.data.results,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default GetLogs;

View File

@@ -1,26 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/logs/getLogsAggregate';
const GetLogsAggregate = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.get(`/logs/aggregate`, {
params: props,
});
return {
statusCode: 200,
error: null,
message: '',
payload: data.data.items,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default GetLogsAggregate;

View File

@@ -1,24 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps } from 'types/api/logs/getSearchFields';
const GetSearchFields = async (): Promise<
SuccessResponse<PayloadProps> | ErrorResponse
> => {
try {
const data = await axios.get(`/logs/fields`);
return {
statusCode: 200,
error: null,
message: '',
payload: data.data,
};
} catch (error) {
return ErrorResponseHandler(error as AxiosError);
}
};
export default GetSearchFields;

View File

@@ -1,23 +0,0 @@
import axios from 'api';
import { ErrorResponseHandler } from 'api/ErrorResponseHandler';
import { AxiosError } from 'axios';
import { ErrorResponse, SuccessResponse } from 'types/api';
import { PayloadProps, Props } from 'types/api/logs/addToSelectedFields';
const removeSelectedField = async (
props: Props,
): Promise<SuccessResponse<PayloadProps> | ErrorResponse> => {
try {
const data = await axios.post(`/logs/fields`, props);
return {
statusCode: 200,
error: null,
message: '',
payload: data.data,
};
} catch (error) {
return Promise.reject(ErrorResponseHandler(error as AxiosError));
}
};
export default removeSelectedField;

View File

@@ -1,22 +0,0 @@
import apiV1 from 'api/apiV1';
import getLocalStorageKey from 'api/browser/localstorage/get';
import { ENVIRONMENT } from 'constants/env';
import { LOCALSTORAGE } from 'constants/localStorage';
import { EventSourcePolyfill } from 'event-source-polyfill';
import { withBasePath } from 'utils/basePath';
// 10 min in ms
const TIMEOUT_IN_MS = 10 * 60 * 1000;
export const LiveTail = (queryParams: string): EventSourcePolyfill =>
new EventSourcePolyfill(
ENVIRONMENT.baseURL
? `${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`
: withBasePath(`${apiV1}logs/tail?${queryParams}`),
{
headers: {
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,
},
heartbeatTimeout: TIMEOUT_IN_MS,
},
);

View File

@@ -1,12 +0,0 @@
import { ReactNode } from 'react';
import { CategoryHeadingText } from './styles';
interface ICategoryHeadingProps {
children: ReactNode;
}
function CategoryHeading({ children }: ICategoryHeadingProps): JSX.Element {
return <CategoryHeadingText color="muted">{children}</CategoryHeadingText>;
}
export default CategoryHeading;

View File

@@ -1,6 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
export const CategoryHeadingText = styled(Typography.Text)`
font-size: 0.8rem;
`;

View File

@@ -1,33 +0,0 @@
import { CSSProperties } from 'react';
import { Color } from '@signozhq/design-tokens';
import { TableProps } from 'antd';
export function getDefaultCellStyle(isDarkMode?: boolean): CSSProperties {
return {
paddingTop: 4,
paddingBottom: 6,
paddingRight: 8,
paddingLeft: 8,
color: isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400,
fontSize: '14px',
fontStyle: 'normal',
fontWeight: 400,
lineHeight: '18px',
letterSpacing: '-0.07px',
marginBottom: '0px',
minWidth: '10rem',
width: 'auto',
};
}
export const defaultTableStyle: CSSProperties = {
minWidth: '40rem',
};
export const defaultListViewPanelStyle: CSSProperties = {
maxWidth: '40rem',
};
export const tableScroll: TableProps<Record<string, unknown>>['scroll'] = {
x: true,
};

View File

@@ -1,24 +0,0 @@
import { Table } from 'antd';
// config
import { tableScroll } from './config';
import { LogsTableViewProps } from './types';
import { useTableView } from './useTableView';
function LogsTableView(props: LogsTableViewProps): JSX.Element {
const { dataSource, columns } = useTableView(props);
return (
<Table
size="small"
columns={columns}
dataSource={dataSource}
pagination={false}
rowKey="id"
bordered
scroll={tableScroll}
/>
);
}
export default LogsTableView;

View File

@@ -1,32 +0,0 @@
import { Color } from '@signozhq/design-tokens';
import { FontSize } from 'container/OptionsMenu/types';
import styled from 'styled-components';
interface TableBodyContentProps {
linesPerRow: number;
fontSize: FontSize;
isDarkMode?: boolean;
}
export const TableBodyContent = styled.div<TableBodyContentProps>`
margin-bottom: 0;
color: ${(props): string =>
props.isDarkMode ? Color.BG_VANILLA_100 : Color.BG_INK_400};
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: ${(props): number => props.linesPerRow};
line-clamp: ${(props): number => props.linesPerRow};
-webkit-box-orient: vertical;
${({ fontSize }): string =>
fontSize === FontSize.SMALL
? `font-size:11px; line-height:16px;`
: fontSize === FontSize.MEDIUM
? `font-size:13px; line-height:20px;`
: `font-size:14px; line-height:24px;`}
`;

View File

@@ -1,40 +1,5 @@
import {
TableColumnsType as ColumnsType,
TableColumnType as ColumnType,
} from 'antd';
import { FontSize } from 'container/OptionsMenu/types';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { TableColumnType as ColumnType } from 'antd';
export type ColumnTypeRender<T = unknown> = ReturnType<
NonNullable<ColumnType<T>['render']>
>;
export type LogsTableViewProps = {
logs: ILog[];
fields: IField[];
linesPerRow: number;
fontSize: FontSize;
onClickExpand?: (log: ILog) => void;
};
export type UseTableViewResult = {
columns: ColumnsType<Record<string, unknown>>;
dataSource: Record<string, string>[];
};
export type UseTableViewProps = {
appendTo?: 'center' | 'end';
onOpenLogsContext?: (log: ILog) => void;
onClickExpand?: (log: ILog) => void;
activeLog?: ILog | null;
activeLogIndex?: number;
activeContextLog?: ILog | null;
isListViewPanel?: boolean;
} & LogsTableViewProps;
export type ActionsColumnProps = {
logId: string;
logs: ILog[];
onOpenLogsContext?: (log: ILog) => void;
};

View File

@@ -1,59 +0,0 @@
.text {
color: var(--l2-foreground);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px; /* 128.571% */
letter-spacing: -0.07px;
&.small {
font-size: 11px;
line-height: 16px;
}
&.medium {
font-size: 13px;
line-height: 20px;
}
&.large {
font-size: 14px;
line-height: 24px;
}
}
.state-indicator {
width: 15px;
.log-state-indicator {
padding: 0px;
}
}
.table-timestamp {
display: flex;
align-items: center;
.timestamp-text {
color: var(--l1-foreground);
margin: 0 !important;
}
}
.paragraph {
margin: 0;
padding: 0px !important;
&.small {
font-size: 11px !important;
line-height: 16px !important;
}
&.medium {
font-size: 13px !important;
line-height: 20px !important;
}
&.large {
font-size: 14px !important;
line-height: 24px !important;
}
}

View File

@@ -1,180 +0,0 @@
import { useMemo } from 'react';
import { TableColumnsType as ColumnsType } from 'antd';
import cx from 'classnames';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
import {
defaultListViewPanelStyle,
defaultTableStyle,
getDefaultCellStyle,
} from './config';
import { TableBodyContent } from './styles';
import {
ColumnTypeRender,
UseTableViewProps,
UseTableViewResult,
} from './types';
import './useTableView.styles.scss';
export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
const {
logs,
fields,
linesPerRow,
fontSize,
appendTo = 'center',
isListViewPanel,
} = props;
const isDarkMode = useIsDarkMode();
const flattenLogData = useMemo(
() => logs.map((log) => FlatLogData(log)),
[logs],
);
const { formatTimezoneAdjustedTimestamp } = useTimezone();
const bodyColumnStyle = useMemo(
() => ({
...defaultTableStyle,
...(fields.length > 2 ? { width: 'auto' } : {}),
}),
[fields.length],
);
const columns: ColumnsType<Record<string, unknown>> = useMemo(() => {
const fieldColumns: ColumnsType<Record<string, unknown>> = fields
.filter((e) => !['id', 'body', 'timestamp'].includes(e.name))
.map(({ name }) => ({
title: name,
dataIndex: name,
accessorKey: name,
id: name.toLowerCase().replace(/\./g, '_'),
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: {
...(isListViewPanel
? defaultListViewPanelStyle
: getDefaultCellStyle(isDarkMode)),
display: '-webkit-box',
WebkitLineClamp: linesPerRow,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
wordBreak: 'break-all',
},
},
children: <p className={cx('paragraph', fontSize)}>{field}</p>,
}),
}));
if (isListViewPanel) {
return [...fieldColumns];
}
return [
{
// We do not need any title and data index for the log state indicator
title: '',
dataIndex: '',
key: 'state-indicator',
accessorKey: 'state-indicator',
id: 'state-indicator',
render: (_, item): ColumnTypeRender<Record<string, unknown>> => ({
children: (
<div className={cx('state-indicator', fontSize)}>
<LogStateIndicator
fontSize={fontSize}
severityText={item.severity_text as string}
severityNumber={item.severity_number as number}
/>
</div>
),
}),
},
...(fields.some((field) => field.name === 'timestamp')
? [
{
title: 'timestamp',
dataIndex: 'timestamp',
key: 'timestamp',
accessorKey: 'timestamp',
id: 'timestamp',
// https://github.com/ant-design/ant-design/discussions/36886
render: (
field: string | number,
): ColumnTypeRender<Record<string, unknown>> => {
const date =
typeof field === 'string'
? formatTimezoneAdjustedTimestamp(
field,
DATE_TIME_FORMATS.ISO_DATETIME_MS,
)
: formatTimezoneAdjustedTimestamp(
field / 1e6,
DATE_TIME_FORMATS.ISO_DATETIME_MS,
);
return {
children: (
<div className="table-timestamp">
<p className={cx('timestamp-text text', fontSize)}>{date}</p>
</div>
),
};
},
},
]
: []),
...(appendTo === 'center' ? fieldColumns : []),
...(fields.some((field) => field.name === 'body')
? [
{
title: 'body',
dataIndex: 'body',
key: 'body',
accessorKey: 'body',
id: 'body',
render: (
field: string | number,
): ColumnTypeRender<Record<string, unknown>> => ({
props: {
style: bodyColumnStyle,
},
children: (
<TableBodyContent
dangerouslySetInnerHTML={{
__html: getSanitizedLogBody(field as string, {
shouldEscapeHtml: true,
}),
}}
fontSize={fontSize}
linesPerRow={linesPerRow}
isDarkMode={isDarkMode}
/>
),
}),
},
]
: []),
...(appendTo === 'end' ? fieldColumns : []),
];
}, [
fields,
isListViewPanel,
appendTo,
isDarkMode,
linesPerRow,
fontSize,
formatTimezoneAdjustedTimestamp,
bodyColumnStyle,
]);
return { columns, dataSource: flattenLogData };
};

View File

@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
import { Button, InputNumber, Popover, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import cx from 'classnames';
import { LogViewMode } from 'container/LogsTable';
import { LogViewMode } from 'container/OptionsMenu/types';
import { FontSize, OptionsMenuConfig } from 'container/OptionsMenu/types';
import {
Check,

View File

@@ -1,5 +1,4 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import CustomSelect from '../CustomSelect';
@@ -204,21 +203,4 @@ describe('CustomSelect Component', () => {
// Check onChange was called
expect(handleChange).toHaveBeenCalled();
});
it('tells the consumer its search was cleared when the dropdown closes', async () => {
// The component clears its own search text on close. A consumer running a
// server-side search needs to hear that, or its results outlive the dropdown.
const onSearch = jest.fn();
const user = userEvent.setup();
render(<CustomSelect options={mockOptions} onSearch={onSearch} />);
const selectElement = screen.getByRole('combobox');
await user.click(selectElement);
await user.type(selectElement, 'opt');
expect(onSearch).toHaveBeenLastCalledWith('opt');
await user.keyboard('{Escape}');
expect(onSearch).toHaveBeenLastCalledWith('');
});
});

View File

@@ -258,10 +258,6 @@ $custom-border-color: #2c3044;
overflow: hidden;
.group-label {
display: flex;
align-items: center;
gap: 4px;
font-weight: 500;
padding: 4px 12px;
font-size: 13px;
@@ -446,7 +442,7 @@ $custom-border-color: #2c3044;
.group-label {
display: flex;
align-items: center;
gap: 4px;
justify-content: space-between;
font-weight: 500;
padding: 4px 12px;

View File

@@ -37,7 +37,6 @@ const ROUTES = {
NOT_FOUND: '/not-found',
LOGS_BASE: '/logs',
LOGS: '/logs/logs-explorer',
OLD_LOGS_EXPLORER: '/logs/old-logs-explorer',
LOGS_EXPLORER: '/logs/logs-explorer',
LIVE_LOGS: '/logs/logs-explorer/live',
LOGS_PIPELINES: '/logs/pipelines',

View File

@@ -2,7 +2,7 @@
display: flex;
flex-direction: column;
gap: var(--spacing-4);
padding: var(--spacing-2) var(--spacing-2);
padding: var(--spacing-2) var(--spacing-8);
--tabs-content-padding: 0;
--tabs-text-color: var(--l1-foreground);

View File

@@ -1,13 +0,0 @@
.llmObservability {
display: flex;
flex-direction: column;
height: 100%;
margin-top: var(--spacing-2);
margin-left: var(--spacing-2);
--tabs-text-color: var(--l1-foreground);
--tabs-active-text-color: var(--l1-foreground);
[role='tabpanel'] {
margin: 0;
padding: var(--spacing-0) var(--spacing-4);
}
}

View File

@@ -1,24 +0,0 @@
import { Tabs } from '@signozhq/ui/tabs';
import { useLLMObservabilityTabs } from './hooks/useLLMObservabilityTabs';
import styles from './LLMObservability.module.scss';
// Shell for the LLM Observability page: renders the top-level tab bar
// (Overview / Configuration) using the SigNoz design-system Tabs, with
// route-driven active state from useLLMObservabilityTabs.
function LLMObservability(): JSX.Element {
const { items, activeTab, onTabChange } = useLLMObservabilityTabs();
return (
<div className={styles.llmObservability} data-testid="llm-observability-page">
<Tabs
items={items}
value={activeTab}
onChange={onTabChange}
testId="llm-observability-tabs"
/>
</div>
);
}
export default LLMObservability;

View File

@@ -6,7 +6,13 @@
//TODO: remove this once we have a proper dashboard page.
// The embedded V2 DashboardContainer renders its own page header (dashboard
// title + share/feedback chrome) removed it for now
:global([class*='dashboardPageHeader']) {
:global([class*='dashboardPageHeader']),
:global([class*='dashboardInfoWithActions']) {
display: none;
}
// Embedded read-only: the add-variable and panel-action triggers are dead here.
:global([class*='addSlot']),
:global([data-testid^='panel-actions-']) {
display: none;
}
}

View File

@@ -4,6 +4,7 @@
gap: var(--spacing-8);
--tabs-content-padding: 0;
margin-top: var(--spacing-3);
padding: var(--spacing-0) var(--spacing-8);
--tabs-text-color: var(--l1-foreground);
--tabs-active-text-color: var(--l1-foreground);
}

View File

@@ -18,6 +18,8 @@
.filtersBarSource {
width: 160px;
/* Trigger defaults to 2.25rem; the search input and Add button are both 2rem. */
--select-trigger-height: 32px;
}
.pageError {

View File

@@ -1,71 +0,0 @@
import { useCallback } from 'react';
import { useLocation } from 'react-router-dom';
import { type TabItemProps } from '@signozhq/ui/tabs';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import LLMObservabilityAttributeMapping from '../AttributeMapping/LLMObservabilityAttributeMapping';
import Explorer from '../Explorer/Explorer';
import Overview from '../Overview/Overview';
import LLMObservabilityModelPricing from '../Settings/ModelPricing/LLMObservabilityModelPricing';
const OVERVIEW_KEY = ROUTES.AI_OBSERVABILITY_OVERVIEW;
const EXPLORER_KEY = ROUTES.AI_OBSERVABILITY_EXPLORER;
const CONFIGURATION_KEY = ROUTES.AI_OBSERVABILITY_CONFIGURATION;
const ATTRIBUTE_MAPPING_KEY = ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING;
interface UseLLMObservabilityTabsResult {
items: TabItemProps[];
activeTab: string;
onTabChange: (key: string) => void;
}
// Drives the top-level LLM Observability tabs. Route-driven: the active tab is
// derived from the pathname (each tab owns a URL) and changing tabs navigates,
// so tabs stay shareable/back-button friendly while rendering with the SigNoz
// design-system Tabs.
export function useLLMObservabilityTabs(): UseLLMObservabilityTabsResult {
const { pathname } = useLocation();
const { safeNavigate } = useSafeNavigate();
let activeTab: string = OVERVIEW_KEY;
if (pathname.startsWith(CONFIGURATION_KEY)) {
activeTab = CONFIGURATION_KEY;
} else if (pathname.startsWith(ATTRIBUTE_MAPPING_KEY)) {
activeTab = ATTRIBUTE_MAPPING_KEY;
} else if (pathname.startsWith(EXPLORER_KEY)) {
activeTab = EXPLORER_KEY;
}
const onTabChange = useCallback(
(key: string): void => {
safeNavigate(key);
},
[safeNavigate],
);
const items: TabItemProps[] = [
{
key: OVERVIEW_KEY,
label: 'Overview',
children: <Overview />,
},
{
key: EXPLORER_KEY,
label: 'Explorer',
children: <Explorer />,
},
{
key: CONFIGURATION_KEY,
label: 'Model pricing',
children: <LLMObservabilityModelPricing />,
},
{
key: ATTRIBUTE_MAPPING_KEY,
label: 'Attribute Mapping',
children: <LLMObservabilityAttributeMapping />,
},
];
return { items, activeTab, onTabChange };
}

View File

@@ -1,136 +0,0 @@
import { memo, useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { Button, Flex } from 'antd';
import { Divider } from '@signozhq/ui/divider';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import Controls from 'container/Controls';
import Download from 'container/Download/Download';
import { getGlobalTime } from 'container/LogsSearchFilter/utils';
import dayjs from 'dayjs';
import { Pagination } from 'hooks/queryPagination';
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
import { FlatLogData } from 'lib/logs/flatLogData';
import { OrderPreferenceItems } from 'pages/Logs/config';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
GET_NEXT_LOG_LINES,
GET_PREVIOUS_LOG_LINES,
RESET_ID_START_AND_END,
SET_LOG_LINES_PER_PAGE,
} from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { Container } from './styles';
import { SkipBack } from '@signozhq/icons';
function LogControls(): JSX.Element | null {
const {
logLinesPerPage,
liveTail,
isLoading: isLogsLoading,
isLoadingAggregate,
logs,
order,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
const handleLogLinesPerPageChange = (e: Pagination['limit']): void => {
dispatch({
type: SET_LOG_LINES_PER_PAGE,
payload: {
logsLinesPerPage: e,
},
});
};
const handleGoToLatest = (): void => {
const { maxTime, minTime } = getMinMaxForSelectedTime(
globalTime.selectedTime,
globalTime.minTime,
globalTime.maxTime,
);
const updatedGlobalTime = getGlobalTime(globalTime.selectedTime, {
maxTime,
minTime,
});
if (updatedGlobalTime) {
dispatch({
type: RESET_ID_START_AND_END,
payload: updatedGlobalTime,
});
}
};
const handleNavigatePrevious = (): void => {
dispatch({
type: GET_PREVIOUS_LOG_LINES,
});
};
const handleNavigateNext = (): void => {
dispatch({
type: GET_NEXT_LOG_LINES,
});
};
const flattenLogData = useMemo(
() =>
logs.map((log) => {
const timestamp =
typeof log.timestamp === 'string'
? dayjs(log.timestamp).format(DATE_TIME_FORMATS.ISO_DATETIME_MS)
: dayjs(log.timestamp / 1e6).format(DATE_TIME_FORMATS.ISO_DATETIME_MS);
return FlatLogData({
...log,
timestamp,
});
}),
[logs],
);
const isLoading = isLogsLoading || isLoadingAggregate;
if (liveTail !== 'STOPPED') {
return null;
}
return (
<Container>
<Download data={flattenLogData} isLoading={isLoading} fileName="log_data" />
<Button
loading={isLoading}
size="small"
type="link"
disabled={order === OrderPreferenceItems.ASC}
onClick={handleGoToLatest}
>
<Flex align="center" gap="4px">
<SkipBack size="md" /> Go to latest
</Flex>
</Button>
<Divider type="vertical" />
<Controls
isLoading={isLoading}
totalCount={logs.length}
countPerPage={logLinesPerPage}
handleNavigatePrevious={handleNavigatePrevious}
handleNavigateNext={handleNavigateNext}
handleCountItemsPerPageChange={handleLogLinesPerPageChange}
/>
</Container>
);
}
export default memo(LogControls);

View File

@@ -1,14 +0,0 @@
import { Button } from 'antd';
import styled from 'styled-components';
export const Container = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
`;
export const DownloadLogButton = styled(Button)`
display: flex;
align-items: center;
`;

View File

@@ -1,6 +1,4 @@
import { useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch } from 'react-redux';
import { generatePath } from 'react-router-dom';
import { Link, Pin } from '@signozhq/icons';
import { Color } from '@signozhq/design-tokens';
@@ -14,23 +12,19 @@ import { ResizeTable } from 'components/ResizeTable';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import { FontSize, OptionsQuery } from 'container/OptionsMenu/types';
import { useIsDarkMode } from 'hooks/useDarkMode';
import history from 'lib/history';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import AppActions from 'types/actions';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { openInNewTab } from 'utils/navigation';
import { ActionItemProps } from './ActionItem';
import { RESTRICTED_SELECTED_FIELDS } from './config';
import FieldRenderer from './FieldRenderer';
import TableViewActions from './TableView/TableViewActions';
import {
@@ -65,7 +59,6 @@ function TableView({
listViewPanelSelectedFields,
handleChangeSelectedView,
}: Props): JSX.Element | null {
const dispatch = useDispatch<Dispatch<AppActions>>();
const [isfilterInLoading, setIsFilterInLoading] = useState<boolean>(false);
const [isfilterOutLoading, setIsFilterOutLoading] = useState<boolean>(false);
const isDarkMode = useIsDarkMode();
@@ -185,11 +178,6 @@ function TableView({
const spanId = flattenLogData?.span_id;
if (traceId) {
dispatch({
type: SET_DETAILED_LOG_DATA,
payload: null,
});
const basePath = generatePath(ROUTES.TRACE_DETAIL, {
id: traceId,
});

View File

@@ -10,7 +10,6 @@ import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { OPERATORS } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { ChangeViewFunctionType } from 'container/ExplorerOptions/types';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ICurrentQueryData } from 'hooks/useHandleExplorerTabChange';
@@ -27,6 +26,7 @@ import {
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { RESTRICTED_SELECTED_FIELDS } from '../config';
import { DataType } from '../TableView';
import {
filterKeyForField,
@@ -141,10 +141,9 @@ export default function TableViewActions(
const { stagedQuery, updateQueriesData } = useQueryBuilder();
const { dataType, logType: fieldType } = getFieldAttributes(record.field);
// there is no option for where clause in old logs explorer and live logs page or infra monitoring
const isOldLogsExplorerOrLiveLogsPage = useMemo(
// there is no option for where clause in live logs page or infra monitoring
const isLiveLogsOrInfraPage = useMemo(
() =>
pathname === ROUTES.OLD_LOGS_EXPLORER ||
pathname === ROUTES.LIVE_LOGS ||
pathname === ROUTES.INFRASTRUCTURE_MONITORING_HOSTS ||
pathname === ROUTES.INFRASTRUCTURE_MONITORING_KUBERNETES,
@@ -400,7 +399,7 @@ export default function TableViewActions(
)}
/>
</Tooltip>
{!isOldLogsExplorerOrLiveLogsPage && (
{!isLiveLogsOrInfraPage && (
<Popover
open={isOpen}
onOpenChange={setIsOpen}
@@ -487,7 +486,7 @@ export default function TableViewActions(
)}
/>
</Tooltip>
{!isOldLogsExplorerOrLiveLogsPage && (
{!isLiveLogsOrInfraPage && (
<Popover
open={isOpen}
onOpenChange={setIsOpen}

View File

@@ -1,9 +1,9 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { RESTRICTED_SELECTED_FIELDS } from 'container/LogsFilters/config';
import { useGetSavedViewParams } from 'hooks/saveViews/useGetSavedViewParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
import { RESTRICTED_SELECTED_FIELDS } from '../../config';
import TableViewActions from '../TableViewActions';
import useAsyncJSONProcessing from '../useAsyncJSONProcessing';

View File

@@ -1,5 +1,10 @@
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
export const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
// Fields that can be filtered on but not grouped by in the log details view.
export const RESTRICTED_GROUP_BY_FIELDS = ['body', 'trace_id'];
export const typeToArrayTypeMapper: { [key in DataTypes]: DataTypes } = {
[DataTypes.String]: DataTypes.ArrayString,
[DataTypes.Float64]: DataTypes.ArrayFloat64,

View File

@@ -61,8 +61,7 @@ export function useLogAttributeActions({
featureFlags?.find((flag) => flag.name === FeatureKeys.USE_JSON_BODY)
?.active || false;
const isOldExplorerOrLive =
pathname === ROUTES.OLD_LOGS_EXPLORER || pathname === ROUTES.LIVE_LOGS;
const isLiveLogs = pathname === ROUTES.LIVE_LOGS;
const filterFor = useCallback(
(context: FieldContext, isFilterIn: boolean): void => {
@@ -221,7 +220,7 @@ export function useLogAttributeActions({
!handleChangeSelectedView ||
!buildLogFilterTarget(fieldKeyPath, undefined, isBodyJsonQueryEnabled)
.groupBySupported ||
isOldExplorerOrLive,
isLiveLogs,
},
{
key: LogDetailsAction.REPLACE_FILTER,
@@ -229,9 +228,7 @@ export function useLogAttributeActions({
icon: <RefreshCw size={12} />,
onClick: replaceFilter,
shouldHide: (_key, fieldKeyPath): boolean =>
!handleChangeSelectedView ||
isRestricted(fieldKeyPath) ||
isOldExplorerOrLive,
!handleChangeSelectedView || isRestricted(fieldKeyPath) || isLiveLogs,
},
];
}, [
@@ -239,7 +236,7 @@ export function useLogAttributeActions({
groupBy,
replaceFilter,
isBodyJsonQueryEnabled,
isOldExplorerOrLive,
isLiveLogs,
handleChangeSelectedView,
onApplyLogFilter,
]);

View File

@@ -1,168 +0,0 @@
import { memo, useCallback } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect, useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router-dom';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
import ROUTES from 'constants/routes';
import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
import getStep from 'lib/getStep';
import { getIdConditions } from 'pages/Logs/utils';
// eslint-disable-next-line no-restricted-imports
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogs } from 'store/actions/logs/getLogs';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
SET_DETAILED_LOG_DATA,
SET_SEARCH_QUERY_STRING,
TOGGLE_LIVE_TAIL,
} from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
type LogDetailedViewProps = {
getLogs: (props: Parameters<typeof getLogs>[0]) => ReturnType<typeof getLogs>;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => ReturnType<typeof getLogsAggregate>;
};
function LogDetailedView({
getLogs,
getLogsAggregate,
}: LogDetailedViewProps): JSX.Element {
const history = useHistory();
const {
detailedLog,
searchFilter: { queryString },
logLinesPerPage,
idStart,
liveTail,
idEnd,
order,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
const onDrawerClose = (): void => {
dispatch({
type: SET_DETAILED_LOG_DATA,
payload: null,
});
};
const handleAddToQuery = useCallback(
(fieldKey: string, fieldValue: string, operator: string) => {
const newOperator = getOldLogsOperatorFromNew(operator);
const updatedQueryString = getGeneratedFilterQueryString(
fieldKey,
fieldValue,
newOperator,
queryString,
);
history.replace(`${ROUTES.OLD_LOGS_EXPLORER}?q=${updatedQueryString}`);
},
[history, queryString],
);
const handleClickActionItem = useCallback(
(fieldKey: string, fieldValue: string, operator: string): void => {
const newOperator = getOldLogsOperatorFromNew(operator);
const updatedQueryString = getGeneratedFilterQueryString(
fieldKey,
fieldValue,
newOperator,
queryString,
);
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: {
searchQueryString: updatedQueryString,
},
});
if (liveTail === 'STOPPED') {
getLogs({
q: updatedQueryString,
limit: logLinesPerPage,
orderBy: 'timestamp',
order,
timestampStart: minTime,
timestampEnd: maxTime,
...getIdConditions(idStart, idEnd, order),
});
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: updatedQueryString,
});
} else if (liveTail === 'PLAYING') {
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: 'PAUSED',
});
setTimeout(
() =>
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: liveTail,
}),
0,
);
}
},
[
dispatch,
getLogs,
getLogsAggregate,
idEnd,
idStart,
liveTail,
logLinesPerPage,
maxTime,
minTime,
order,
queryString,
],
);
return (
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={detailedLog}
onClose={onDrawerClose}
onAddToQuery={handleAddToQuery}
onClickActionItem={handleClickActionItem}
/>
);
}
interface DispatchProps {
getLogs: (props: Parameters<typeof getLogs>[0]) => (dispatch: never) => void;
getLogsAggregate: (
props: Parameters<typeof getLogsAggregate>[0],
) => (dispatch: never) => void;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogs: bindActionCreators(getLogs, dispatch),
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
});
export default connect(null, mapDispatchToProps)(memo(LogDetailedView as any));

View File

@@ -5,10 +5,6 @@ import {
QUERY_BUILDER_FUNCTIONS,
} from 'constants/antlrQueryConstants';
import { OPERATORS as QUERY_BUILDER_OPERATORS } from 'constants/queryBuilder';
import {
RESTRICTED_GROUP_BY_FIELDS,
RESTRICTED_SELECTED_FIELDS,
} from 'container/LogsFilters/config';
import { MetricsType } from 'container/MetricsApplication/constant';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
@@ -18,6 +14,10 @@ import {
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
RESTRICTED_GROUP_BY_FIELDS,
RESTRICTED_SELECTED_FIELDS,
} from './config';
import { LogAttributeBucket } from './constants';
import { generateFieldKeyForArray, getDataTypes } from './utils';

View File

@@ -1,26 +0,0 @@
export const TIME_PICKER_OPTIONS = [
{
value: 5,
label: '5m',
},
{
value: 15,
label: '15m',
},
{
value: 30,
label: '30m',
},
{
value: 60,
label: '1hr',
},
{
value: 360,
label: '6hrs',
},
{
value: 720,
label: '12hrs',
},
];

View File

@@ -1,270 +0,0 @@
import { useCallback, useEffect, useMemo, useRef } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect, useDispatch, useSelector } from 'react-redux';
import { green } from '@ant-design/colors';
import { Pause, Play, EllipsisVertical } from '@signozhq/icons';
import { Button, Flex, Popover, Select, Space } from 'antd';
import { LiveTail } from 'api/logs/livetail';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { useNotifications } from 'hooks/useNotifications';
import getStep from 'lib/getStep';
import { throttle } from 'lodash-es';
// eslint-disable-next-line no-restricted-imports
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { UPDATE_AUTO_REFRESH_DISABLED } from 'types/actions/globalTime';
import {
FLUSH_LOGS,
PUSH_LIVE_TAIL_EVENT,
SET_LIVE_TAIL_START_TIME,
SET_LOADING,
TOGGLE_LIVE_TAIL,
} from 'types/actions/logs';
import { TLogsLiveTailState } from 'types/api/logs/liveTail';
import { ILog } from 'types/api/logs/log';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { popupContainer } from 'utils/selectPopupContainer';
import { TIME_PICKER_OPTIONS } from './config';
import { StopContainer, TimePickerCard, TimePickerSelect } from './styles';
function LogLiveTail({ getLogsAggregate }: Props): JSX.Element {
const {
liveTail,
searchFilter: { queryString },
liveTailStartRange,
logs,
idEnd,
idStart,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const isDarkMode = useIsDarkMode();
const { selectedAutoRefreshInterval } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { notifications } = useNotifications();
const dispatch = useDispatch<Dispatch<AppActions>>();
const handleLiveTail = (toggleState: TLogsLiveTailState): void => {
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: toggleState,
});
dispatch({
type: UPDATE_AUTO_REFRESH_DISABLED,
payload: toggleState === 'PLAYING',
});
};
const batchedEventsRef = useRef<ILog[]>([]);
const pushLiveLog = useCallback(() => {
dispatch({
type: PUSH_LIVE_TAIL_EVENT,
payload: batchedEventsRef.current.reverse(),
});
batchedEventsRef.current = [];
}, [dispatch]);
const pushLiveLogThrottled = useMemo(
() => throttle(pushLiveLog, 1000),
[pushLiveLog],
);
const batchLiveLog = useCallback(
(e: { data: string }): void => {
batchedEventsRef.current.push(JSON.parse(e.data as string) as never);
pushLiveLogThrottled();
},
[pushLiveLogThrottled],
);
const firstLogsId = useMemo(() => logs[0]?.id, [logs]);
// This ref depicts thats whether the live tail is played from paused state or not.
const liveTailSourceRef = useRef<EventSource>();
useEffect(() => {
if (liveTail === 'PLAYING') {
const timeStamp = dayjs().subtract(liveTailStartRange, 'minute').valueOf();
const queryParams = new URLSearchParams({
...(queryString ? { q: queryString } : {}),
timestampStart: (timeStamp * 1e6) as never,
...(liveTailSourceRef.current && firstLogsId
? {
idGt: firstLogsId,
}
: {}),
});
if (liveTailSourceRef.current) {
liveTailSourceRef.current.close();
}
const source = LiveTail(queryParams.toString());
liveTailSourceRef.current = source;
source.onmessage = function connectionMessage(e): void {
batchLiveLog(e);
};
source.onerror = function connectionError(event: unknown): void {
console.error(event);
source.close();
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: 'STOPPED',
});
dispatch({
type: SET_LOADING,
payload: false,
});
notifications.error({
message: 'Live tail stopped due to some error.',
});
};
}
if (liveTail === 'STOPPED') {
liveTailSourceRef.current = undefined;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [liveTail, queryString, notifications, dispatch]);
const handleLiveTailStart = (): void => {
handleLiveTail('PLAYING');
const startTime =
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
const endTime = dayjs().valueOf() * 1e6;
getLogsAggregate({
timestampStart: startTime,
timestampEnd: endTime,
step: getStep({
start: startTime,
end: endTime,
inputFormat: 'ns',
}),
q: queryString,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
if (!liveTailSourceRef.current) {
dispatch({
type: FLUSH_LOGS,
});
}
};
const OptionsPopOverContent = useMemo(
() => (
<TimePickerSelect
getPopupContainer={popupContainer}
disabled={liveTail === 'PLAYING'}
value={liveTailStartRange}
onChange={(value): void => {
if (typeof value === 'number') {
dispatch({
type: SET_LIVE_TAIL_START_TIME,
payload: value,
});
}
}}
>
{TIME_PICKER_OPTIONS.map((optionData) => (
<Select.Option key={optionData.label} value={optionData.value}>
Last {optionData.label}
</Select.Option>
))}
</TimePickerSelect>
),
[dispatch, liveTail, liveTailStartRange],
);
const isDisabled = useMemo(
() => selectedAutoRefreshInterval?.length > 0,
[selectedAutoRefreshInterval],
);
const onLiveTailStop = (): void => {
handleLiveTail('STOPPED');
dispatch({
type: UPDATE_AUTO_REFRESH_DISABLED,
payload: false,
});
dispatch({
type: SET_LOADING,
payload: false,
});
if (liveTailSourceRef.current) {
liveTailSourceRef.current.close();
}
};
return (
<TimePickerCard>
<Space size={0} align="center">
{liveTail === 'PLAYING' ? (
<Button
type="primary"
onClick={onLiveTailStop}
title="Pause live tail"
style={{ background: green[6] }}
>
<Flex align="center" gap={4}>
<span>Pause</span>
<Pause size="md" />
</Flex>
</Button>
) : (
<Button
type="primary"
onClick={handleLiveTailStart}
title="Start live tail"
disabled={isDisabled}
>
<Flex align="center" gap={4}>
Go Live <Play size="md" />
</Flex>
</Button>
)}
{liveTail !== 'STOPPED' && (
<Button type="dashed" onClick={onLiveTailStop} title="Exit live tail">
<StopContainer isDarkMode={isDarkMode} />
</Button>
)}
<Popover
getPopupContainer={popupContainer}
placement="bottomRight"
title="Select Live Tail Timing"
trigger="click"
content={OptionsPopOverContent}
>
<EllipsisVertical size="lg" />
</Popover>
</Space>
</TimePickerCard>
);
}
interface DispatchProps {
getLogsAggregate: typeof getLogsAggregate;
}
type Props = DispatchProps;
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
});
export default connect(null, mapDispatchToProps)(LogLiveTail);

View File

@@ -1,25 +0,0 @@
import { Card, Select } from 'antd';
import styled from 'styled-components';
export const TimePickerCard = styled(Card)`
.ant-card-body {
display: flex;
padding: 0;
}
`;
export const TimePickerSelect = styled(Select)`
min-width: 100px;
`;
interface Props {
isDarkMode: boolean;
}
export const StopContainer = styled.div<Props>`
height: 0.8rem;
width: 0.8rem;
border-radius: 0.1rem;
background-color: ${({ isDarkMode }): string =>
isDarkMode ? '#fff' : '#000'};
`;

View File

@@ -1,95 +0,0 @@
import { useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect, useSelector } from 'react-redux';
import { blue } from '@ant-design/colors';
import Graph from 'components/Graph';
import Spinner from 'components/Spinner';
import dayjs from 'dayjs';
import useInterval from 'hooks/useInterval';
import getStep from 'lib/getStep';
// eslint-disable-next-line no-restricted-imports
import { bindActionCreators } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import { ILogsReducer } from 'types/reducer/logs';
import { Container } from './styles';
function LogsAggregate({ getLogsAggregate }: DispatchProps): JSX.Element {
const {
searchFilter: { queryString },
idEnd,
idStart,
isLoadingAggregate,
logsAggregate,
liveTail,
liveTailStartRange,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
useInterval(
() => {
const startTime =
dayjs().subtract(liveTailStartRange, 'minute').valueOf() * 1e6;
const endTime = dayjs().valueOf() * 1e6;
getLogsAggregate({
timestampStart: startTime,
timestampEnd: endTime,
step: getStep({
start: startTime,
end: endTime,
inputFormat: 'ns',
}),
q: queryString,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
},
60000,
liveTail === 'PLAYING',
);
const graphData = useMemo(
() => ({
labels: logsAggregate.map((s) => new Date(s.timestamp / 1000000)),
datasets: [
{
data: logsAggregate.map((s) => s.value),
backgroundColor: blue[4],
},
],
}),
[logsAggregate],
);
return (
<Container>
{isLoadingAggregate ? (
<Spinner size="default" height="100%" />
) : (
<Graph
name="usage"
data={graphData}
type="bar"
containerHeight="100%"
animate
/>
)}
</Container>
);
}
interface DispatchProps {
getLogsAggregate: typeof getLogsAggregate;
}
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
});
export default connect(null, mapDispatchToProps)(LogsAggregate);

View File

@@ -1,11 +0,0 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const Container = styled(Card)`
position: relative;
margin: 0.5rem 0;
.ant-card-body {
height: 20vh;
min-height: 200px;
}
`;

View File

@@ -1,93 +0,0 @@
import { ReactNode, useCallback, useMemo, useState } from 'react';
import { Loader } from '@signozhq/icons';
import { Button, Popover, Spin } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { useIsDarkMode } from 'hooks/useDarkMode';
import {
IField,
IInterestingFields,
ISelectedFields,
} from 'types/api/logs/fields';
import { ICON_STYLE } from './config';
import { Field } from './styles';
function FieldItem({
name,
buttonIcon,
buttonOnClick,
fieldData,
fieldIndex,
isLoading,
iconHoverText,
}: FieldItemProps): JSX.Element {
const [isHovered, setIsHovered] = useState<boolean>(false);
const isDarkMode = useIsDarkMode();
const onClickHandler = useCallback(() => {
if (!isLoading && buttonOnClick) {
buttonOnClick({ fieldData, fieldIndex });
}
}, [buttonOnClick, fieldData, fieldIndex, isLoading]);
const renderContent = useMemo(() => {
if (isLoading) {
return (
<Spin
spinning
size="small"
indicator={<Loader className="animate-spin" />}
/>
);
}
if (isHovered) {
return (
<Popover content={<Typography>{iconHoverText}</Typography>}>
<Button
size="small"
type="text"
icon={buttonIcon}
onClick={onClickHandler}
/>
</Popover>
);
}
return null;
}, [buttonIcon, iconHoverText, isHovered, isLoading, onClickHandler]);
const onMouseHoverHandler = useCallback(
(value: boolean) => (): void => {
setIsHovered(value);
},
[],
);
return (
<Field
onMouseEnter={onMouseHoverHandler(true)}
onMouseLeave={onMouseHoverHandler(false)}
isDarkMode={isDarkMode}
>
<Typography style={ICON_STYLE.PLUS}>{name}</Typography>
{renderContent}
</Field>
);
}
interface FieldItemProps {
name: string;
buttonIcon: ReactNode;
buttonOnClick: (props: {
fieldData: IInterestingFields | ISelectedFields;
fieldIndex: number;
}) => void;
fieldData: IField;
fieldIndex: number;
isLoading: boolean;
iconHoverText: string;
}
export default FieldItem;

View File

@@ -1,11 +0,0 @@
import { blue, red } from '@ant-design/colors';
export const RESTRICTED_SELECTED_FIELDS = ['timestamp', 'id'];
// Fields that can be filtered on but not grouped by in the log details view.
export const RESTRICTED_GROUP_BY_FIELDS = ['body', 'trace_id'];
export const ICON_STYLE = {
PLUS: { color: blue[5] },
CLOSE: { color: red[5] },
};

View File

@@ -1,119 +0,0 @@
import { ChangeEvent, useCallback, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { CirclePlus, X } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Col } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import { fieldSearchFilter } from 'lib/logs/fieldSearch';
import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs';
import { ICON_STYLE, RESTRICTED_SELECTED_FIELDS } from './config';
import FieldItem from './FieldItem';
import { CategoryContainer, FieldContainer } from './styles';
import { IHandleInterestProps, IHandleRemoveInterestProps } from './types';
import { onHandleAddInterest, onHandleRemoveInterest } from './utils';
function LogsFilters(): JSX.Element {
const {
fields: { interesting, selected },
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const [selectedFieldLoading, setSelectedFieldLoading] = useState<number[]>([]);
const [interestingFieldLoading, setInterestingFieldLoading] = useState<
number[]
>([]);
const [filterValuesInput, setFilterValuesInput] = useState('');
const handleSearch = (e: ChangeEvent<HTMLInputElement>): void => {
setFilterValuesInput((e.target as HTMLInputElement).value);
};
const onHandleAddSelectedToInteresting = useCallback(
({ fieldData, fieldIndex }: IHandleInterestProps) =>
(): Promise<void> =>
onHandleAddInterest({
fieldData,
fieldIndex,
interesting,
interestingFieldLoading,
setInterestingFieldLoading,
selected,
}),
[interesting, interestingFieldLoading, selected],
);
const onHandleRemoveSelected = useCallback(
({ fieldData, fieldIndex }: IHandleRemoveInterestProps) =>
(): Promise<void> =>
onHandleRemoveInterest({
fieldData,
fieldIndex,
interesting,
interestingFieldLoading,
selected,
setSelectedFieldLoading,
}),
[interesting, interestingFieldLoading, selected, setSelectedFieldLoading],
);
return (
<Col flex="250px">
<Input
placeholder="Filter Values"
onInput={handleSearch}
value={filterValuesInput}
onChange={handleSearch}
/>
<CategoryContainer>
<CategoryHeading>SELECTED FIELDS</CategoryHeading>
<FieldContainer>
{selected
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
.filter((field) => RESTRICTED_SELECTED_FIELDS.indexOf(field.name) === -1)
.map((field, idx) => (
<FieldItem
key={`${JSON.stringify(field)}`}
name={field.name}
fieldData={field}
fieldIndex={idx}
buttonIcon={<X style={ICON_STYLE.CLOSE} size="md" />}
buttonOnClick={onHandleRemoveSelected({
fieldData: field,
fieldIndex: idx,
})}
isLoading={selectedFieldLoading.includes(idx)}
iconHoverText="Remove from Selected Fields"
/>
))}
</FieldContainer>
</CategoryContainer>
<CategoryContainer>
<CategoryHeading>INTERESTING FIELDS</CategoryHeading>
<FieldContainer>
{interesting
.filter((field) => fieldSearchFilter(field.name, filterValuesInput))
.map((field, idx) => (
<FieldItem
key={`${JSON.stringify(field)}`}
name={field.name}
fieldData={field}
fieldIndex={idx}
buttonIcon={<CirclePlus style={ICON_STYLE.PLUS} size="md" />}
buttonOnClick={onHandleAddSelectedToInteresting({
fieldData: field,
fieldIndex: idx,
})}
isLoading={interestingFieldLoading.includes(idx)}
iconHoverText="Add to Selected Fields"
/>
))}
</FieldContainer>
</CategoryContainer>
</Col>
);
}
export default LogsFilters;

View File

@@ -1,29 +0,0 @@
import { blue, grey } from '@ant-design/colors';
import { Typography } from '@signozhq/ui/typography';
import styled from 'styled-components';
export const CategoryContainer = styled.div`
margin: 1rem 0;
padding-left: 0.2rem;
`;
export const FieldContainer = styled(Typography.Text)`
margin: 0.2rem 0;
color: ${blue[4]};
`;
export const Field = styled.div<{ isDarkMode: boolean }>`
border-radius: 0.5rem;
padding: 0.3rem 0.5rem;
height: 2rem;
display: flex;
justify-content: space-between;
align-items: center;
&:hover {
background: ${({ isDarkMode }): string => (isDarkMode ? grey[7] : '#ddd')};
}
`;
export const ExtractField = styled(Typography.Text)`
color: ${blue[4]};
`;

View File

@@ -1,36 +0,0 @@
import { SetStateAction } from 'react';
import {
IField,
IInterestingFields,
ISelectedFields,
} from 'types/api/logs/fields';
type SetLoading = (value: SetStateAction<number[]>) => void;
export type IHandleInterestProps = {
fieldData: IInterestingFields;
fieldIndex: number;
};
export type IHandleRemoveInterestProps = {
fieldData: ISelectedFields;
fieldIndex: number;
};
export interface OnHandleAddInterestProps {
setInterestingFieldLoading: SetLoading;
fieldIndex: number;
fieldData: ISelectedFields;
interesting: IField[];
interestingFieldLoading: number[];
selected: IField[];
}
export interface OnHandleRemoveInterestProps {
setSelectedFieldLoading: SetLoading;
selected: IField[];
interesting: IField[];
interestingFieldLoading: number[];
fieldData: IInterestingFields;
fieldIndex: number;
}

View File

@@ -1,105 +0,0 @@
import { message } from 'antd';
import addToSelectedFields from 'api/logs/AddToSelectedField';
import removeSelectedField from 'api/logs/RemoveFromSelectedField';
import store from 'store';
import {
UPDATE_INTERESTING_FIELDS,
UPDATE_SELECTED_FIELDS,
} from 'types/actions/logs';
import { ErrorResponse } from 'types/api';
import { RESTRICTED_SELECTED_FIELDS } from './config';
import { OnHandleAddInterestProps, OnHandleRemoveInterestProps } from './types';
export const onHandleAddInterest = async ({
setInterestingFieldLoading,
fieldIndex,
fieldData,
interesting,
interestingFieldLoading,
selected,
}: OnHandleAddInterestProps): Promise<void> => {
const { dispatch } = store;
setInterestingFieldLoading((prevState: number[]) => {
prevState.push(fieldIndex);
return [...prevState];
});
try {
await addToSelectedFields({
...fieldData,
selected: true,
});
dispatch({
type: UPDATE_INTERESTING_FIELDS,
payload: {
field: interesting.filter((e) => e.name !== fieldData.name),
type: 'selected',
},
});
dispatch({
type: UPDATE_SELECTED_FIELDS,
payload: {
field: [...selected, fieldData],
type: 'selected',
},
});
} catch (errRes) {
message.error((errRes as ErrorResponse)?.error);
} finally {
setInterestingFieldLoading(
interestingFieldLoading.filter((e) => e !== fieldIndex),
);
}
};
export const onHandleRemoveInterest = async ({
setSelectedFieldLoading,
selected,
interesting,
interestingFieldLoading,
fieldData,
fieldIndex,
}: OnHandleRemoveInterestProps): Promise<void> => {
if (RESTRICTED_SELECTED_FIELDS.includes(fieldData.name)) {
return;
}
const { dispatch } = store;
setSelectedFieldLoading((prevState) => {
prevState.push(fieldIndex);
return [...prevState];
});
try {
await removeSelectedField({
...fieldData,
selected: false,
});
dispatch({
type: UPDATE_SELECTED_FIELDS,
payload: {
field: selected.filter((e) => e.name !== fieldData.name),
type: 'selected',
},
});
dispatch({
type: UPDATE_INTERESTING_FIELDS,
payload: {
field: [...interesting, fieldData],
type: 'interesting',
},
});
} catch (errRes) {
message.error((errRes as ErrorResponse)?.error);
} finally {
setSelectedFieldLoading(
interestingFieldLoading.filter((e) => e !== fieldIndex),
);
}
};

View File

@@ -1,27 +0,0 @@
import { Button, Row } from 'antd';
interface SearchFieldsActionBarProps {
applyUpdate: VoidFunction;
clearFilters: VoidFunction;
}
export function SearchFieldsActionBar({
applyUpdate,
clearFilters,
}: SearchFieldsActionBarProps): JSX.Element | null {
return (
<Row style={{ justifyContent: 'flex-end', paddingRight: '2.4rem' }}>
<Button
type="default"
onClick={clearFilters}
style={{ marginRight: '1rem' }}
>
Clear Filter
</Button>
<Button type="primary" onClick={applyUpdate}>
Apply
</Button>
</Row>
);
}
export default SearchFieldsActionBar;

View File

@@ -1,19 +0,0 @@
import { Typography } from '@signozhq/ui/typography';
interface FieldKeyProps {
name: string;
type: string;
}
function FieldKey({ name, type }: FieldKeyProps): JSX.Element {
return (
<span style={{ margin: '0.25rem 0', display: 'flex', gap: '0.5rem' }}>
<Typography.Text>{name}</Typography.Text>
<Typography.Text color="muted" italic>
{type}
</Typography.Text>
</span>
);
}
export default FieldKey;

View File

@@ -1,259 +0,0 @@
import { useCallback, useMemo, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { SquareX, X } from '@signozhq/icons';
import { Input } from '@signozhq/ui/input';
import { Button, Select } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import {
ConditionalOperators,
QueryOperatorsMultiVal,
QueryOperatorsSingleVal,
} from 'lib/logql/tokens';
import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs';
import FieldKey from '../FieldKey';
import { QueryFieldContainer } from '../styles';
import { QueryFields } from '../utils';
import { Container, QueryWrapper } from './styles';
const { Option } = Select;
function QueryConditionField({
query,
queryIndex,
onUpdate,
}: QueryConditionFieldProps): JSX.Element {
const allOptions = Object.values(ConditionalOperators);
return (
<Select
defaultValue={
(query as QueryFields).value &&
(
(query as QueryFields)
?.value as unknown as QueryFields as unknown as string
).toUpperCase()
}
onChange={(e): void => {
onUpdate({ ...query, value: e }, queryIndex);
}}
>
{allOptions.map((cond) => (
<Option key={cond} value={cond} label={cond}>
{cond}
</Option>
))}
</Select>
);
}
interface QueryFieldProps {
query: Query;
queryIndex: number;
onUpdate: (query: Query, queryIndex: number) => void;
onDelete: (queryIndex: number) => void;
}
function QueryField({
query,
queryIndex,
onUpdate,
onDelete,
}: QueryFieldProps): JSX.Element | null {
const [isDropDownOpen, setIsDropDownOpen] = useState(false);
const {
fields: { selected },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const getFieldType = useCallback(
(inputKey: string): string => {
const selectedField = selected.find((field) => inputKey === field.name);
if (selectedField) {
return selectedField.type;
}
return '';
},
[selected],
);
const fieldType = useMemo(
() => getFieldType(query[0].value as string),
[getFieldType, query],
);
const handleChange = (qIdx: number, value: string): void => {
const updatedQuery = [...query];
updatedQuery[qIdx].value = value || '';
if (qIdx === 1) {
if (Object.values(QueryOperatorsMultiVal).includes(value)) {
if (!Array.isArray(updatedQuery[2].value)) {
updatedQuery[2].value = [];
}
} else if (
Object.values(QueryOperatorsSingleVal).includes(value) &&
Array.isArray(updatedQuery[2].value)
) {
updatedQuery[2].value = '';
}
}
onUpdate(updatedQuery, queryIndex);
};
const handleClear = (): void => {
onDelete(queryIndex);
};
if (!Array.isArray(query)) {
return null;
}
return (
<QueryFieldContainer
style={{ ...(queryIndex === 0 && { gridColumnStart: 2 }) }}
>
<div style={{ flex: 1, minWidth: 100 }}>
<FieldKey name={(query[0] && query[0].value) as string} type={fieldType} />
</div>
<Select
defaultActiveFirstOption={false}
placeholder="Select Operator"
defaultValue={
query[1] && query[1].value
? (query[1].value as string).toUpperCase()
: null
}
onChange={(e): void => handleChange(1, e)}
style={{ minWidth: 150 }}
>
{Object.values({
...QueryOperatorsMultiVal,
...QueryOperatorsSingleVal,
}).map((cond) => (
<Option key={cond} value={cond} label={cond}>
{cond}
</Option>
))}
</Select>
<div style={{ flex: 2 }}>
{Array.isArray(query[2].value) ||
Object.values(QueryOperatorsMultiVal).some(
(op) => op.toUpperCase() === (query[1].value as string)?.toUpperCase(),
) ? (
<Select
mode="tags"
style={{ width: '100%' }}
open={isDropDownOpen}
onChange={(e): void => handleChange(2, e as never)}
defaultValue={(query[2] && query[2].value) || []}
notFoundContent={null}
onInputKeyDown={(): void => setIsDropDownOpen(true)}
onSelect={(): void => setIsDropDownOpen(false)}
/>
) : (
<Input
onChange={(e): void => {
handleChange(2, e.target.value);
}}
style={{ width: '100%' }}
defaultValue={query[2] && query[2].value}
value={query[2] && query[2].value}
/>
)}
</div>
<Button
icon={<X size="md" />}
type="text"
size="small"
onClick={handleClear}
/>
</QueryFieldContainer>
);
}
interface QueryConditionFieldProps {
query: QueryFields;
queryIndex: number;
onUpdate: (arg0: unknown, arg1: number) => void;
}
export type Query = { value: string | string[]; type: string }[];
export interface QueryBuilderProps {
keyPrefix: string;
onDropDownToggleHandler: (value: boolean) => VoidFunction;
fieldsQuery: QueryFields[][];
setFieldsQuery: (q: QueryFields[][]) => void;
syncKeyPrefix: () => void;
}
function QueryBuilder({
keyPrefix,
fieldsQuery,
setFieldsQuery,
onDropDownToggleHandler,
syncKeyPrefix,
}: QueryBuilderProps): JSX.Element {
const handleUpdate = (query: Query, queryIndex: number): void => {
const updated = [...fieldsQuery];
updated[queryIndex] = query as never; // parseQuery(query) as never;
setFieldsQuery(updated);
};
const handleDelete = (queryIndex: number): void => {
const updated = [...fieldsQuery];
if (queryIndex !== 0) {
updated.splice(queryIndex - 1, 2);
} else {
updated.splice(queryIndex, 2);
}
setFieldsQuery(updated);
// initiate re-render query panel
syncKeyPrefix();
};
const QueryUI = (
fieldsQuery: QueryFields[][],
): JSX.Element | JSX.Element[] => {
const result: JSX.Element[] = [];
fieldsQuery.forEach((query, idx) => {
if (Array.isArray(query) && query.length > 1) {
result.push(
<QueryField
key={keyPrefix}
query={query}
queryIndex={idx}
onUpdate={handleUpdate}
onDelete={handleDelete}
/>,
);
} else {
result.push(
<div key={keyPrefix}>
<QueryConditionField
query={Array.isArray(query) ? query[0] : query}
queryIndex={idx}
onUpdate={handleUpdate as never}
/>
</div>,
);
}
});
return result;
};
return (
<>
<Container isMargin={fieldsQuery.length === 0}>
<CategoryHeading>LOG QUERY BUILDER</CategoryHeading>
<SquareX onClick={onDropDownToggleHandler(false)} size="md" />
</Container>
<QueryWrapper key={keyPrefix}>{QueryUI(fieldsQuery)}</QueryWrapper>
</>
);
}
export default QueryBuilder;

View File

@@ -1,17 +0,0 @@
import styled from 'styled-components';
interface Props {
isMargin: boolean;
}
export const Container = styled.div<Props>`
display: flex;
justify-content: space-between;
width: 100%;
margin-bottom: ${(props): string => (props.isMargin ? '2rem' : '0')};
`;
export const QueryWrapper = styled.div`
display: grid;
grid-template-columns: 80px 1fr;
margin: 0.5rem 0px;
`;

View File

@@ -1,62 +0,0 @@
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Button } from 'antd';
import CategoryHeading from 'components/Logs/CategoryHeading';
import map from 'lodash-es/map';
import { AppState } from 'store/reducers';
// import { ADD_SEARCH_FIELD_QUERY_STRING } from 'types/actions/logs';
import { ILogsReducer } from 'types/reducer/logs';
import FieldKey from './FieldKey';
interface SuggestedItemProps {
name: string;
type: string;
applySuggestion: (name: string) => void;
}
function SuggestedItem({
name,
type,
applySuggestion,
}: SuggestedItemProps): JSX.Element {
const addSuggestedField = (): void => {
applySuggestion(name);
};
return (
<Button
type="text"
style={{ display: 'block', padding: '0.2rem' }}
onClick={addSuggestedField}
>
<FieldKey name={name} type={type} />
</Button>
);
}
interface SuggestionsProps {
applySuggestion: (name: string) => void;
}
function Suggestions({ applySuggestion }: SuggestionsProps): JSX.Element {
const {
fields: { selected },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
return (
<div>
<CategoryHeading>SUGGESTIONS</CategoryHeading>
<div>
{map(selected, (field) => (
<SuggestedItem
key={JSON.stringify(field)}
name={field.name}
type={field.type}
applySuggestion={applySuggestion}
/>
))}
</div>
</div>
);
}
export default Suggestions;

View File

@@ -1,123 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { useNotifications } from 'hooks/useNotifications';
import { reverseParser } from 'lib/logql';
import { flatten } from 'lodash-es';
import { AppState } from 'store/reducers';
import { ILogsReducer } from 'types/reducer/logs';
import { SearchFieldsActionBar } from './ActionBar';
import QueryBuilder from './QueryBuilder/QueryBuilder';
import Suggestions from './Suggestions';
import {
createParsedQueryStructure,
fieldsQueryIsvalid,
hashCode,
initQueryKOVPair,
prepareConditionOperator,
QueryFields,
} from './utils';
export interface SearchFieldsProps {
onDropDownToggleHandler: (value: boolean) => VoidFunction;
updateQueryString: (value: string) => void;
}
function SearchFields({
onDropDownToggleHandler,
updateQueryString,
}: SearchFieldsProps): JSX.Element {
const {
searchFilter: { parsedQuery },
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const [fieldsQuery, setFieldsQuery] = useState(
createParsedQueryStructure([...parsedQuery] as never[]),
);
const keyPrefixRef = useRef(hashCode(JSON.stringify(fieldsQuery)));
const { notifications } = useNotifications();
useEffect(() => {
const updatedFieldsQuery = createParsedQueryStructure([
...parsedQuery,
] as never[]);
setFieldsQuery(updatedFieldsQuery);
const incomingHashCode = hashCode(JSON.stringify(updatedFieldsQuery));
if (incomingHashCode !== keyPrefixRef.current) {
keyPrefixRef.current = incomingHashCode;
}
}, [parsedQuery]);
// syncKeyPrefix initiates re-render. useful in situations like
// delete field (in search panel). this method allows condiitonally
// setting keyPrefix as doing it on every update of query initiates
// a re-render. this is a problem for text fields where input focus goes away.
const syncKeyPrefix = (): void => {
keyPrefixRef.current = hashCode(JSON.stringify(fieldsQuery));
};
const addSuggestedField = useCallback(
(name: string): void => {
if (!name) {
return;
}
const query = [...fieldsQuery];
if (fieldsQuery.length > 0) {
query.push([prepareConditionOperator()]);
}
const newField: QueryFields[] = [];
initQueryKOVPair(name).forEach((q) => newField.push(q));
query.push(newField);
keyPrefixRef.current = hashCode(JSON.stringify(query));
setFieldsQuery(query);
},
[fieldsQuery, setFieldsQuery],
);
const applyUpdate = useCallback((): void => {
const flatParsedQuery = flatten(fieldsQuery);
if (!fieldsQueryIsvalid(flatParsedQuery)) {
notifications.error({
message: 'Please enter a valid criteria for each of the selected fields',
});
return;
}
keyPrefixRef.current = hashCode(JSON.stringify(flatParsedQuery));
updateQueryString(reverseParser(flatParsedQuery));
onDropDownToggleHandler(false)();
}, [fieldsQuery, notifications, onDropDownToggleHandler, updateQueryString]);
const clearFilters = useCallback((): void => {
keyPrefixRef.current = hashCode(JSON.stringify([]));
setFieldsQuery([]);
updateQueryString('');
}, [updateQueryString]);
return (
<>
<QueryBuilder
key={keyPrefixRef.current}
keyPrefix={keyPrefixRef.current}
onDropDownToggleHandler={onDropDownToggleHandler}
fieldsQuery={fieldsQuery}
setFieldsQuery={setFieldsQuery}
syncKeyPrefix={syncKeyPrefix}
/>
<SearchFieldsActionBar
applyUpdate={applyUpdate}
clearFilters={clearFilters}
/>
<Suggestions applySuggestion={addSuggestedField} />
</>
);
}
export default SearchFields;

View File

@@ -1,16 +0,0 @@
import { blue } from '@ant-design/colors';
import styled from 'styled-components';
export const QueryFieldContainer = styled.div`
padding: 0.25rem 0.5rem;
margin: 0.1rem 0.5rem 0;
display: flex;
flex-direction: row;
align-items: center;
border-radius: 0.25rem;
gap: 1rem;
width: 100%;
&:hover {
background: ${blue[6]};
}
`;

View File

@@ -1,137 +0,0 @@
// @ts-nocheck
import {
ConditionalOperators,
QueryTypes,
ValidTypeSequence,
ValidTypeValue,
} from 'lib/logql/tokens';
export interface QueryFields {
type: keyof typeof QueryTypes;
value: string | string[];
}
export function fieldsQueryIsvalid(queryFields: QueryFields[]): boolean {
let lastOp: string;
let result = true;
queryFields.forEach((q, idx) => {
if (!q.value || q.value === null || q.value === '') {
result = false;
}
if (Array.isArray(q.value) && q.value.length === 0) {
result = false;
}
const nextOp = idx < queryFields.length ? queryFields[idx + 1] : undefined;
if (!ValidTypeSequence(lastOp?.type, q?.type, nextOp?.type)) {
result = false;
}
if (!ValidTypeValue(lastOp?.value, q.value)) {
result = false;
}
lastOp = q;
});
return result;
}
export const queryKOVPair = (): QueryFields[] => [
{
type: QueryTypes.QUERY_KEY,
value: null,
},
{
type: QueryTypes.QUERY_OPERATOR,
value: null,
},
{
type: QueryTypes.QUERY_VALUE,
value: null,
},
];
export const initQueryKOVPair = (
name: string = null,
op: string = null,
value: string | string[] = null,
): QueryFields[] => [
{
type: QueryTypes.QUERY_KEY,
value: name,
},
{
type: QueryTypes.QUERY_OPERATOR,
value: op,
},
{
type: QueryTypes.QUERY_VALUE,
value: value,
},
];
export const prepareConditionOperator = (
op: string = ConditionalOperators.AND,
): QueryFields => {
return {
type: QueryTypes.CONDITIONAL_OPERATOR,
value: op,
};
};
export const createParsedQueryStructure = (
parsedQuery = [],
): QueryFields[][] => {
if (parsedQuery.length === 0) {
return parsedQuery;
}
const structuredArray = [queryKOVPair()];
let cond;
let qCtr = -1;
parsedQuery.forEach((query) => {
if (cond) {
structuredArray.push(cond);
structuredArray.push(queryKOVPair());
cond = null;
qCtr = -1;
}
const stagingArr = structuredArray.at(-1);
const prevQuery =
Array.isArray(stagingArr) && qCtr >= 0 ? stagingArr[qCtr] : null;
if (query.type === QueryTypes.QUERY_KEY) {
stagingArr[qCtr + 1] = query;
} else if (
query.type === QueryTypes.QUERY_OPERATOR &&
prevQuery &&
prevQuery.type === QueryTypes.QUERY_KEY
) {
stagingArr[qCtr + 1] = query;
} else if (
query.type === QueryTypes.QUERY_VALUE &&
prevQuery &&
prevQuery.type === QueryTypes.QUERY_OPERATOR
) {
stagingArr[qCtr + 1] = query;
} else if (query.type === QueryTypes.CONDITIONAL_OPERATOR) {
cond = query;
}
qCtr++;
});
return structuredArray;
};
export const hashCode = (s: string): string => {
if (!s) {
return '0';
}
return `${Math.abs(
[...s].reduce((a, b) => {
a = (a << 5) - a + b.codePointAt(0);
return a & a;
}, 0),
)}`;
};

View File

@@ -1,230 +0,0 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect, useDispatch, useSelector } from 'react-redux';
import { Input, InputRef, Popover } from 'antd';
import useUrlQuery from 'hooks/useUrlQuery';
import getStep from 'lib/getStep';
import debounce from 'lodash-es/debounce';
import { getIdConditions } from 'pages/Logs/utils';
// eslint-disable-next-line no-restricted-imports
import { bindActionCreators, Dispatch } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import { GetLogsFields } from 'store/actions/logs/getFields';
import { getLogs } from 'store/actions/logs/getLogs';
import { getLogsAggregate } from 'store/actions/logs/getLogsAggregate';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
FLUSH_LOGS,
SET_LOADING,
SET_LOADING_AGGREGATE,
TOGGLE_LIVE_TAIL,
} from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { popupContainer } from 'utils/selectPopupContainer';
import SearchFields from './SearchFields';
import { Container, DropDownContainer } from './styles';
import { useSearchParser } from './useSearchParser';
function SearchFilter({
getLogs,
getLogsAggregate,
getLogsFields,
}: SearchFilterProps): JSX.Element {
const { updateQueryString, queryString } = useSearchParser();
const [searchText, setSearchText] = useState(queryString);
const [showDropDown, setShowDropDown] = useState(false);
const searchRef = useRef<InputRef>(null);
const { logLinesPerPage, idEnd, idStart, liveTail, order } = useSelector<
AppState,
ILogsReducer
>((state) => state.logs);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const dispatch = useDispatch<Dispatch<AppActions>>();
// keep sync with url queryString
useEffect(() => {
setSearchText(queryString);
}, [queryString]);
const debouncedupdateQueryString = useMemo(
() => debounce(updateQueryString, 300),
[updateQueryString],
);
const onDropDownToggleHandler = useCallback(
(value: boolean) => (): void => {
setShowDropDown(value);
},
[],
);
const handleSearch = useCallback(
(customQuery: string) => {
getLogsFields();
const { maxTime, minTime } = globalTime;
if (liveTail === 'PLAYING') {
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: 'PAUSED',
});
dispatch({
type: FLUSH_LOGS,
});
dispatch({
type: TOGGLE_LIVE_TAIL,
payload: liveTail,
});
dispatch({
type: SET_LOADING,
payload: false,
});
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: customQuery,
...(idStart ? { idGt: idStart } : {}),
...(idEnd ? { idLt: idEnd } : {}),
});
} else {
getLogs({
q: customQuery,
limit: logLinesPerPage,
orderBy: 'timestamp',
order,
timestampStart: minTime,
timestampEnd: maxTime,
...getIdConditions(idStart, idEnd, order),
});
getLogsAggregate({
timestampStart: minTime,
timestampEnd: maxTime,
step: getStep({
start: minTime,
end: maxTime,
inputFormat: 'ns',
}),
q: customQuery,
});
}
},
[
dispatch,
getLogs,
getLogsAggregate,
idEnd,
idStart,
liveTail,
logLinesPerPage,
globalTime,
getLogsFields,
order,
],
);
const urlQuery = useUrlQuery();
const urlQueryString = urlQuery.get('q');
useEffect(() => {
dispatch({
type: SET_LOADING,
payload: true,
});
dispatch({
type: SET_LOADING_AGGREGATE,
payload: true,
});
const debouncedHandleSearch = debounce(handleSearch, 600);
debouncedHandleSearch(urlQueryString || '');
return (): void => {
debouncedHandleSearch.cancel();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
urlQueryString,
idEnd,
idStart,
logLinesPerPage,
dispatch,
globalTime.maxTime,
globalTime.minTime,
order,
]);
const onPopOverChange = useCallback(
(isVisible: boolean) => {
onDropDownToggleHandler(isVisible)();
},
[onDropDownToggleHandler],
);
return (
<Container>
<Popover
getPopupContainer={popupContainer}
placement="bottom"
content={
<DropDownContainer>
<SearchFields
updateQueryString={updateQueryString}
onDropDownToggleHandler={onDropDownToggleHandler}
/>
</DropDownContainer>
}
trigger="click"
overlayInnerStyle={{
width: `${searchRef?.current?.input?.offsetWidth || 0}px`,
}}
open={showDropDown}
destroyTooltipOnHide
onOpenChange={onPopOverChange}
>
<Input.Search
ref={searchRef}
placeholder="Search Filter"
value={searchText}
onChange={(e): void => {
const { value } = e.target;
setSearchText(value);
}}
onSearch={debouncedupdateQueryString}
allowClear
/>
</Popover>
</Container>
);
}
interface DispatchProps {
getLogs: typeof getLogs;
getLogsAggregate: typeof getLogsAggregate;
getLogsFields: typeof GetLogsFields;
}
type SearchFilterProps = DispatchProps;
const mapDispatchToProps = (
dispatch: ThunkDispatch<unknown, unknown, AppActions>,
): DispatchProps => ({
getLogs: bindActionCreators(getLogs, dispatch),
getLogsAggregate: bindActionCreators(getLogsAggregate, dispatch),
getLogsFields: bindActionCreators(GetLogsFields, dispatch),
});
export default connect(null, mapDispatchToProps)(memo(SearchFilter));

View File

@@ -1,14 +0,0 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const DropDownContainer = styled(Card)`
.ant-card-body {
width: 100%;
}
`;
export const Container = styled.div`
width: 100%;
flex: 1;
position: relative;
`;

View File

@@ -1,81 +0,0 @@
import { useCallback, useEffect } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { QueryParams } from 'constants/query';
import useUrlQuery from 'hooks/useUrlQuery';
import { getMinMaxForSelectedTime } from 'lib/getMinMax';
import history from 'lib/history';
import { parseQuery } from 'lib/logql';
import isEqual from 'lodash-es/isEqual';
// eslint-disable-next-line no-restricted-imports
import { Dispatch } from 'redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
SET_SEARCH_QUERY_PARSED_PAYLOAD,
SET_SEARCH_QUERY_STRING,
} from 'types/actions/logs';
import { GlobalReducer } from 'types/reducer/globalTime';
import { ILogsReducer } from 'types/reducer/logs';
import { getGlobalTime } from './utils';
export function useSearchParser(): {
queryString: string;
parsedQuery: unknown;
updateQueryString: (arg0: string) => void;
} {
const dispatch = useDispatch<Dispatch<AppActions>>();
const {
searchFilter: { parsedQuery, queryString },
order,
} = useSelector<AppState, ILogsReducer>((store) => store.logs);
const urlQuery = useUrlQuery();
const parsedFilters = urlQuery.get('q');
const { minTime, maxTime, selectedTime } = useSelector<
AppState,
GlobalReducer
>((store) => store.globalTime);
const updateQueryString = useCallback(
(updatedQueryString: string) => {
history.replace({
pathname: history.location.pathname,
search: `?${QueryParams.q}=${updatedQueryString}&${QueryParams.order}=${order}`,
});
const globalTime = getMinMaxForSelectedTime(selectedTime, minTime, maxTime);
dispatch({
type: SET_SEARCH_QUERY_STRING,
payload: {
searchQueryString: updatedQueryString,
globalTime: getGlobalTime(selectedTime, globalTime),
},
});
const parsedQueryFromString = parseQuery(updatedQueryString);
if (!isEqual(parsedQuery, parsedQueryFromString)) {
dispatch({
type: SET_SEARCH_QUERY_PARSED_PAYLOAD,
payload: parsedQueryFromString,
});
}
},
// need to hide this warning as we don't want to update the query string on every change
// eslint-disable-next-line react-hooks/exhaustive-deps
[dispatch, parsedQuery, selectedTime, queryString],
);
useEffect(() => {
updateQueryString(parsedFilters || '');
}, [parsedFilters, updateQueryString]);
return {
queryString,
parsedQuery,
updateQueryString,
};
}

View File

@@ -1,15 +0,0 @@
import {
CustomTimeType,
Time,
} from 'container/TopNav/DateTimeSelectionV2/types';
import { GetMinMaxPayload } from 'lib/getMinMax';
export const getGlobalTime = (
selectedTime: Time | CustomTimeType,
globalTime: GetMinMaxPayload,
): GetMinMaxPayload | undefined => {
if (selectedTime === 'custom') {
return undefined;
}
return globalTime;
};

View File

@@ -1,164 +0,0 @@
import { memo, useCallback, useMemo } from 'react';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import { Virtuoso } from 'react-virtuoso';
import { Card } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import LogDetail from 'components/LogDetail';
import { VIEW_TYPES } from 'components/LogDetail/constants';
// components
import ListLogView from 'components/Logs/ListLogView';
import RawLogView from 'components/Logs/RawLogView';
import LogsTableView from 'components/Logs/TableView';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import Spinner from 'components/Spinner';
import { CARD_BODY_STYLE } from 'constants/card';
import { LOCALSTORAGE } from 'constants/localStorage';
import { useOptionsMenu } from 'container/OptionsMenu';
import { useActiveLog } from 'hooks/logs/useActiveLog';
import { AppState } from 'store/reducers';
import { DataSource, StringOperators } from 'types/common/queryBuilder';
// interfaces
import { ILogsReducer } from 'types/reducer/logs';
// styles
import { Container, Heading } from './styles';
import './logsTable.styles.scss';
export type LogViewMode = 'raw' | 'table' | 'list';
type LogsTableProps = {
viewMode: LogViewMode;
linesPerRow: number;
};
function LogsTable(props: LogsTableProps): JSX.Element {
const { viewMode, linesPerRow } = props;
const { activeLog, onClearActiveLog, onAddToQuery, onSetActiveLog } =
useActiveLog();
const {
logs,
fields: { selected },
isLoading,
liveTail,
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const isLiveTail = useMemo(
() => logs.length === 0 && liveTail === 'PLAYING',
[logs?.length, liveTail],
);
const isNoLogs = useMemo(
() => logs.length === 0 && liveTail === 'STOPPED',
[logs?.length, liveTail],
);
const { options } = useOptionsMenu({
storageKey: LOCALSTORAGE.LOGS_LIST_OPTIONS,
// this component will alwyays be called on old logs explorer page itself!
dataSource: DataSource.LOGS,
// and we do not have table / timeseries aggregated views in the old logs explorer!
aggregateOperator: StringOperators.NOOP,
});
const getItemContent = useCallback(
(index: number): JSX.Element => {
const log = logs[index];
if (viewMode === 'raw') {
return (
<RawLogView
key={log.id}
data={log}
linesPerRow={linesPerRow}
selectedFields={selected}
fontSize={options.fontSize}
/>
);
}
return (
<ListLogView
key={log.id}
logData={log}
selectedFields={selected}
linesPerRow={linesPerRow}
onAddToQuery={onAddToQuery}
onSetActiveLog={onSetActiveLog}
fontSize={options.fontSize}
/>
);
},
[
logs,
viewMode,
selected,
linesPerRow,
onAddToQuery,
onSetActiveLog,
options.fontSize,
],
);
const renderContent = useMemo(() => {
if (viewMode === 'table') {
return (
<LogsTableView
onClickExpand={onSetActiveLog}
logs={logs}
fields={selected}
linesPerRow={linesPerRow}
fontSize={options.fontSize}
/>
);
}
return (
<Card className="logs-card" bodyStyle={CARD_BODY_STYLE}>
<OverlayScrollbar isVirtuoso>
<Virtuoso totalCount={logs.length} itemContent={getItemContent} />
</OverlayScrollbar>
</Card>
);
}, [
getItemContent,
linesPerRow,
logs,
onSetActiveLog,
options.fontSize,
selected,
viewMode,
]);
if (isLoading) {
return <Spinner height={20} tip="Getting Logs" />;
}
return (
<Container>
{viewMode !== 'table' && (
<Heading>
<Typography.Text>Event</Typography.Text>
</Heading>
)}
{isLiveTail && <Typography>Getting live logs...</Typography>}
{isNoLogs && <Typography>No logs lines found</Typography>}
{renderContent}
<LogDetail
selectedTab={VIEW_TYPES.OVERVIEW}
log={activeLog}
onClose={onClearActiveLog}
onAddToQuery={onAddToQuery}
onClickActionItem={onAddToQuery}
/>
</Container>
);
}
export default memo(LogsTable);

View File

@@ -1,3 +0,0 @@
.logs-card {
flex: 1;
}

View File

@@ -1,20 +0,0 @@
import { Card } from 'antd';
import styled from 'styled-components';
export const Container = styled.div`
overflow-x: hidden;
width: 100%;
margin-bottom: 1rem;
margin-top: 0.5rem;
display: flex;
flex-direction: column;
flex: 1;
`;
export const Heading = styled(Card)`
margin-bottom: 0.1rem;
height: 32px;
.ant-card-body {
padding: 0.3rem 0.5rem;
}
`;

View File

@@ -1,7 +1,6 @@
import {
MetricsexplorertypesMetricAlertDTO,
MetricsexplorertypesMetricAttributeDTO,
MetricsexplorertypesMetricDashboardDTO,
MetricsexplorertypesMetricHighlightsResponseDTO,
MetricsexplorertypesMetricMetadataDTO,
MetrictypesTemporalityDTO,
@@ -55,8 +54,6 @@ export type MetricHighlight = MetricsexplorertypesMetricHighlightsResponseDTO;
export type MetricAlert = MetricsexplorertypesMetricAlertDTO;
export type MetricDashboard = MetricsexplorertypesMetricDashboardDTO;
export type MetricMetadata = MetricsexplorertypesMetricMetadataDTO;
export interface MetricMetadataFormState {
type: MetrictypesTypeDTO;

View File

@@ -3,10 +3,9 @@ import './FormatField.styles.scss';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
import { LogViewMode } from 'container/LogsTable';
import { FieldTitle } from '../styles';
import { OptionsMenuConfig } from '../types';
import { LogViewMode, OptionsMenuConfig } from '../types';
import { FormatFieldWrapper } from './styles';
function FormatField({ config }: FormatFieldProps): JSX.Element | null {

View File

@@ -1,6 +1,7 @@
import { InputNumberProps, RadioProps, SelectProps } from 'antd';
import { TelemetryFieldKey } from 'api/v5/v5';
import { LogViewMode } from 'container/LogsTable';
export type LogViewMode = 'raw' | 'table' | 'list';
export enum FontSize {
SMALL = 'small',

View File

@@ -4,7 +4,6 @@ import { useQueries } from 'react-query';
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
import { TelemetryFieldKey } from 'api/v5/v5';
import { AxiosResponse } from 'axios';
import { LogViewMode } from 'container/LogsTable';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import useDebounce from 'hooks/useDebounce';
import { useNotifications } from 'hooks/useNotifications';
@@ -33,6 +32,7 @@ import {
import {
FontSize,
InitialOptions,
LogViewMode,
OptionsMenuConfig,
OptionsQuery,
} from './types';

View File

@@ -285,7 +285,6 @@ export const defaultMoreMenuItems: SidebarItem[] = [
key: ROUTES.API_MONITORING,
label: 'External APIs',
icon: <Binoculars size={16} />,
isNew: true,
isEnabled: true,
itemKey: 'external-apis',
},
@@ -293,7 +292,7 @@ export const defaultMoreMenuItems: SidebarItem[] = [
key: ROUTES.AI_OBSERVABILITY_OVERVIEW,
label: 'AI Observability',
icon: <Brain size={16} />,
isNew: true,
isBeta: true,
// Gated behind the `enable_ai_observability` feature flag in
// SideNav's `computedSecondaryMenuItems`; disabled by default.
isEnabled: false,

View File

@@ -1,29 +1,21 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useQueryClient } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useDispatch, useSelector } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import { getAggregateKeys } from 'api/queryBuilder/getAttributeKeys';
import { SOMETHING_WENT_WRONG } from 'constants/api';
import { QueryParams } from 'constants/query';
import { OPERATORS, QueryBuilderKeys } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { MetricsType } from 'container/MetricsApplication/constant';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useNotifications } from 'hooks/useNotifications';
import useUrlQuery from 'hooks/useUrlQuery';
import { getGeneratedFilterQueryString } from 'lib/getGeneratedFilterQueryString';
import { chooseAutocompleteFromCustomValue } from 'lib/newQueryBuilder/chooseAutocompleteFromCustomValue';
import { AppState } from 'store/reducers';
import { SET_DETAILED_LOG_DATA } from 'types/actions/logs';
import { ILog } from 'types/api/logs/log';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { ILogsReducer } from 'types/reducer/logs';
import { v4 as uuid } from 'uuid';
import { UseActiveLog } from './types';
@@ -40,22 +32,10 @@ export function getOldLogsOperatorFromNew(operator: string): string {
}
// eslint-disable-next-line sonarjs/cognitive-complexity
export const useActiveLog = (): UseActiveLog => {
const dispatch = useDispatch();
const {
searchFilter: { queryString },
} = useSelector<AppState, ILogsReducer>((state) => state.logs);
const queryClient = useQueryClient();
const { pathname } = useLocation();
const history = useHistory();
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
const { notifications } = useNotifications();
const isLogsPage = useMemo(
() => pathname === ROUTES.OLD_LOGS_EXPLORER,
[pathname],
);
const [activeLog, setActiveLog] = useState<ILog | null>(null);
// Close drawer/clear active log when query in URL changes
@@ -72,26 +52,9 @@ export const useActiveLog = (): UseActiveLog => {
prevQueryRef.current = compositeQuery;
}, [compositeQuery]);
const onSetDetailedLogData = useCallback(
(logData: ILog) => {
dispatch({
type: SET_DETAILED_LOG_DATA,
payload: logData,
});
},
[dispatch],
);
const onSetActiveLog = useCallback(
(nextActiveLog: ILog): void => {
if (isLogsPage) {
onSetDetailedLogData(nextActiveLog);
} else {
setActiveLog(nextActiveLog);
}
},
[isLogsPage, onSetDetailedLogData],
);
const onSetActiveLog = useCallback((nextActiveLog: ILog): void => {
setActiveLog(nextActiveLog);
}, []);
const onClearActiveLog = useCallback((): void => setActiveLog(null), []);
@@ -204,26 +167,12 @@ export const useActiveLog = (): UseActiveLog => {
},
[currentQuery, notifications, queryClient, redirectWithQueryBuilderData],
);
const onAddToQueryLogs = useCallback(
(fieldKey: string, fieldValue: string, operator: string) => {
const newOperator = getOldLogsOperatorFromNew(operator);
const updatedQueryString = getGeneratedFilterQueryString(
fieldKey,
fieldValue,
newOperator,
queryString,
);
history.replace(`${ROUTES.OLD_LOGS_EXPLORER}?q=${updatedQueryString}`);
},
[history, queryString],
);
return {
activeLog,
onSetActiveLog,
onClearActiveLog,
onAddToQuery: isLogsPage ? onAddToQueryLogs : onAddToQueryExplorer,
onAddToQuery: onAddToQueryExplorer,
onGroupByAttribute,
};
};

View File

@@ -1,206 +0,0 @@
const logqlQueries = [
{
query: "OPERATION in ('bcd','xy\\'z') AND FULLTEXT contains 'helloxyz'", // Query with IN
splitterQuery: [
'OPERATION',
'in',
"('bcd','xy\\'z')",
'AND',
'FULLTEXT',
'contains',
"'helloxyz'",
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'OPERATION' },
{ type: 'QUERY_OPERATOR', value: 'in' },
{ type: 'QUERY_VALUE', value: ['bcd', "xy\\'z"] },
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'contains' },
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
],
},
{
query: "OPERATION in ('bcd') and FULLTEXT contains 'helloxyz'", // Query with IN
splitterQuery: [
'OPERATION',
'in',
"('bcd')",
'and',
'FULLTEXT',
'contains',
"'helloxyz'",
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'OPERATION' },
{ type: 'QUERY_OPERATOR', value: 'in' },
{ type: 'QUERY_VALUE', value: ['bcd'] },
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'contains' },
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
],
},
{
query: "OPERATION in ('bcd','xyz') AND FULLTEXT contains 'helloxyz'", // Query with IN
splitterQuery: [
'OPERATION',
'in',
"('bcd','xyz')",
'AND',
'FULLTEXT',
'contains',
"'helloxyz'",
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'OPERATION' },
{ type: 'QUERY_OPERATOR', value: 'in' },
{ type: 'QUERY_VALUE', value: ['bcd', 'xyz'] },
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'contains' },
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
],
},
{
query: "status gte 200 AND FULLTEXT contains 'helloxyz'",
splitterQuery: [
'status',
'gte',
'200',
'AND',
'FULLTEXT',
'contains',
"'helloxyz'",
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'status' },
{ type: 'QUERY_OPERATOR', value: 'gte' },
{ type: 'QUERY_VALUE', value: '200' },
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'contains' },
{ type: 'QUERY_VALUE', value: "'helloxyz'" },
],
},
{
query: "service IN ('Hello (\\'World')", // Query with quotes and brackets
splitterQuery: ['service', 'IN', "('Hello (\\'World')"],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'service' },
{ type: 'QUERY_OPERATOR', value: 'IN' },
{ type: 'QUERY_VALUE', value: ["Hello (\\'World"] },
],
},
{
query: "service IN ('Hello (\\'World') AND FULLTEXT contains 'ola'", // Query with full text as key pair
splitterQuery: [
'service',
'IN',
"('Hello (\\'World')",
'AND',
'FULLTEXT',
'contains',
"'ola'",
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'service' },
{ type: 'QUERY_OPERATOR', value: 'IN' },
{ type: 'QUERY_VALUE', value: ["Hello (\\'World"] },
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'contains' },
{ type: 'QUERY_VALUE', value: "'ola'" },
],
},
{
query: 'id lt 100 and id gt 50 and code lte 500 and code gte 400', // Query with numbers
splitterQuery: [
'id',
'lt',
'100',
'and',
'id',
'gt',
'50',
'and',
'code',
'lte',
'500',
'and',
'code',
'gte',
'400',
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'id' },
{ type: 'QUERY_OPERATOR', value: 'lt' },
{ type: 'QUERY_VALUE', value: '100' },
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
{ type: 'QUERY_KEY', value: 'id' },
{ type: 'QUERY_OPERATOR', value: 'gt' },
{ type: 'QUERY_VALUE', value: '50' },
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
{ type: 'QUERY_KEY', value: 'code' },
{ type: 'QUERY_OPERATOR', value: 'lte' },
{ type: 'QUERY_VALUE', value: '500' },
{ type: 'CONDITIONAL_OPERATOR', value: 'and' },
{ type: 'QUERY_KEY', value: 'code' },
{ type: 'QUERY_OPERATOR', value: 'gte' },
{ type: 'QUERY_VALUE', value: '400' },
],
},
{
query: 'FULLTEXT contains \'Hello, "World"\'', // Full text with Quotes
splitterQuery: ['FULLTEXT', 'contains', `'Hello, "World"'`],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'contains' },
{ type: 'QUERY_VALUE', value: `'Hello, "World"'` },
],
},
{
query: "service NIN ('name > 100') AND length gt 100", // Characters inside string
splitterQuery: [
'service',
'NIN',
"('name > 100')",
'AND',
'length',
'gt',
'100',
],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'service' },
{ type: 'QUERY_OPERATOR', value: 'NIN' },
{ type: 'QUERY_VALUE', value: ['name > 100'] },
{ type: 'CONDITIONAL_OPERATOR', value: 'AND' },
{ type: 'QUERY_KEY', value: 'length' },
{ type: 'QUERY_OPERATOR', value: 'gt' },
{ type: 'QUERY_VALUE', value: '100' },
],
},
// Template
{
query: '',
splitterQuery: [],
parsedQuery: [],
},
];
const specialLogQLQueries = [
{
query: 'key IN 22', // Fulltext
splitterQuery: ['key', 'IN', '22'],
parsedQuery: [
{ type: 'QUERY_KEY', value: 'FULLTEXT' },
{ type: 'QUERY_OPERATOR', value: 'CONTAINS' },
{ type: 'QUERY_VALUE', value: 'key IN 22' },
],
reverseParsed: "FULLTEXT CONTAINS 'key IN 22'",
},
];
export { logqlQueries, specialLogQLQueries };

View File

@@ -1,10 +0,0 @@
import { logqlQueries } from 'lib/__fixtures__/logql';
import parser from 'lib/logql/parser';
describe('lib/logql/parser', () => {
it('parse valid queries', () => {
logqlQueries.forEach((queryObject) => {
expect(parser(queryObject.query)).toStrictEqual(queryObject.parsedQuery);
});
});
});

View File

@@ -1,12 +0,0 @@
import { logqlQueries } from 'lib/__fixtures__/logql';
import { reverseParser } from 'lib/logql/reverseParser';
describe('lib/logql/reverseParser', () => {
it('reverse parse valid queries', () => {
logqlQueries.forEach((queryObject) => {
expect(reverseParser(queryObject.parsedQuery)).toStrictEqual(
queryObject.query,
);
});
});
});

View File

@@ -1,10 +0,0 @@
import { logqlQueries } from 'lib/__fixtures__/logql';
import { splitter } from 'lib/logql/splitter';
describe('lib/logql/splitter', () => {
it('splitter valid quereies', () => {
logqlQueries.forEach((queryObject) => {
expect(splitter(queryObject.query)).toStrictEqual(queryObject.splitterQuery);
});
});
});

View File

@@ -1,24 +0,0 @@
import { generateFilterQuery } from './logs/generateFilterQuery';
export const getGeneratedFilterQueryString = (
fieldKey: string,
fieldValue: string,
operator: string,
queryString: string,
): string => {
let updatedQueryString = queryString || '';
const generatedString = generateFilterQuery({
fieldKey,
fieldValue,
type: operator,
});
if (updatedQueryString.length === 0) {
updatedQueryString += `${generatedString}`;
} else {
updatedQueryString += ` AND ${generatedString}`;
}
return updatedQueryString;
};

View File

@@ -1,7 +0,0 @@
export class ErrorConvertToFullText extends Error {
constructor(message: string) {
super(message);
this.name = 'ErrorConvertToFullText';
Object.setPrototypeOf(this, ErrorConvertToFullText.prototype);
}
}

View File

@@ -1,7 +0,0 @@
export class ErrorInvalidQueryPair extends Error {
constructor(message: string) {
super(message);
this.name = 'ErrorInvalidQueryPair';
Object.setPrototypeOf(this, ErrorInvalidQueryPair.prototype);
}
}

View File

@@ -1,2 +0,0 @@
export * from './ConvertToFullText';
export * from './InvalidQueryPair';

View File

@@ -1,4 +0,0 @@
export * from './parser';
export * from './reverseParser';
export * from './splitter';
export * from './tokens';

View File

@@ -1,149 +0,0 @@
// @ts-nocheck
import {
ErrorConvertToFullText,
ErrorInvalidQueryPair,
} from 'lib/logql/errors';
import splitter from 'lib/logql/splitter';
import {
ConditionalOperators,
QueryOperatorsMultiVal,
QueryOperatorsSingleVal,
QueryTypes,
} from 'lib/logql/tokens';
const validateMultiValue = (queryToken: string): boolean => {
const queryValues = [];
let start;
let isQuoteStart = false;
if (queryToken[0] === '(' && queryToken[queryToken.length - 1] === ')') {
for (let idx = 1; idx < queryToken.length - 1; idx += 1) {
if (queryToken[idx] === "'") {
if (queryToken[idx - 1] === '\\') {
// skip
} else if (isQuoteStart) {
isQuoteStart = false;
queryValues.push(queryToken.slice(start, idx));
} else {
isQuoteStart = true;
start = idx + 1;
}
}
}
} else {
return false;
}
return queryValues;
};
// eslint-disable-next-line sonarjs/cognitive-complexity
export const parseQuery = (queryString): any => {
let parsedRaw = [];
const generateQuery = (queryToken): void => {
const prevToken = parsedRaw[parsedRaw.length - 1];
// Is a QUERY_KEY
if (
prevToken === undefined ||
prevToken.type === QueryTypes.CONDITIONAL_OPERATOR
) {
parsedRaw.push({
type: QueryTypes.QUERY_KEY,
value: queryToken,
});
}
// Is a QUERY_OPERATOR
else if (prevToken && prevToken.type === QueryTypes.QUERY_KEY) {
if (
Object.values({
...QueryOperatorsMultiVal,
...QueryOperatorsSingleVal,
}).find((op) => op.toLowerCase() === queryToken.toLowerCase())
) {
parsedRaw.push({
type: QueryTypes.QUERY_OPERATOR,
value: queryToken,
});
} else {
throw new ErrorInvalidQueryPair(
'Expected conditional operator received',
queryToken,
);
}
}
// Is a QUERY_VALUE
else if (prevToken && prevToken.type === QueryTypes.QUERY_OPERATOR) {
// Check for multi value
let value = queryToken;
// if (
// typeof queryToken === 'string' &&
// queryToken.length >= 2 &&
// queryToken[0] === "'" &&
// queryToken[queryToken.length - 1] === "'"
// ) {
// value = queryToken.slice(1, queryToken.length - 1);
// }
if (
Object.values(QueryOperatorsMultiVal).some(
(operator) => operator.toLowerCase() === prevToken.value.toLowerCase(),
)
) {
value = validateMultiValue(queryToken);
if (value === false) {
throw new ErrorConvertToFullText();
}
}
parsedRaw.push({
type: QueryTypes.QUERY_VALUE,
value,
});
} else if (prevToken && prevToken.type === QueryTypes.QUERY_VALUE) {
if (
Object.values(ConditionalOperators).find(
(op) => op.toLowerCase() === queryToken.toLowerCase(),
)
) {
parsedRaw.push({
type: QueryTypes.CONDITIONAL_OPERATOR,
value: queryToken,
});
} else {
throw new ErrorInvalidQueryPair(
'Expected conditional operator received',
queryToken,
);
}
} else {
// Not a Key
}
};
try {
const spaceSplittedQUery = splitter(queryString);
spaceSplittedQUery.forEach((q) => {
generateQuery(q);
});
} catch (e: Error) {
if (e instanceof ErrorInvalidQueryPair) {
//
} else if (e instanceof ErrorConvertToFullText) {
parsedRaw = [
{
type: QueryTypes.QUERY_KEY,
value: 'FULLTEXT',
},
{
type: QueryTypes.QUERY_OPERATOR,
value: 'CONTAINS',
},
{
type: QueryTypes.QUERY_VALUE,
value: String.raw`${queryString}`,
},
];
}
}
return parsedRaw;
};
export default parseQuery;

View File

@@ -1,39 +0,0 @@
// @ts-nocheck
import { QueryTypes, StringTypeQueryOperators } from './tokens';
export const reverseParser = (
parserQueryArr: { type: string; value: any }[] = [],
): string => {
let queryString = '';
let lastToken: { type: string; value: any };
parserQueryArr.forEach((query) => {
if (queryString) {
queryString += ' ';
}
if (Array.isArray(query.value) && query.value.length > 0) {
// if the values are array type, here we spread them in
// ('a', 'b') format
const queryValuesMapped = query.value.map((val) => `'${val}'`).join(',');
queryString += `(${queryValuesMapped})`;
} else {
if (
query.type === QueryTypes.QUERY_VALUE &&
lastToken.type === QueryTypes.QUERY_OPERATOR &&
Object.values(StringTypeQueryOperators).includes(lastToken.value)
) {
// for operators that need string type value, here we append single
// quotes. if the content has single quote they would be removed
queryString += `'${query.value?.replace(/'/g, '')}'`;
} else {
queryString += query.value;
}
}
lastToken = query;
});
return queryString;
};
export default reverseParser;

View File

@@ -1,51 +0,0 @@
// @ts-nocheck
// eslint-disable-next-line sonarjs/cognitive-complexity
export const splitter = (queryString: string): string[] => {
const splittedParts: string[] = [];
let start = 0;
let isBracketStart = false;
let isQuoteStart = false;
const pushPart = (idx): void => {
splittedParts.push(queryString.slice(start, idx));
start = idx + 1;
};
for (let idx = 0; idx < queryString.length; idx += 1) {
const currentChar = queryString[idx];
if (currentChar === ' ') {
if (!isBracketStart && !isQuoteStart) {
pushPart(idx);
}
} else if (currentChar === '(') {
isBracketStart = true;
} else if (currentChar === ')') {
if (queryString[idx - 1] !== '\\') {
pushPart(idx + 1);
isBracketStart = false;
}
if (isQuoteStart) {
isQuoteStart = false;
}
} else if (currentChar === "'") {
if (isQuoteStart) {
if (queryString[idx - 1] !== '\\' && !isBracketStart) {
pushPart(idx + 1);
isQuoteStart = false;
}
} else {
isQuoteStart = true;
}
}
}
// Process remaining part
if (start < queryString.length) {
pushPart(queryString.length);
}
return splittedParts.map((s) => String.raw`${s}`).filter(Boolean);
};
export default splitter;

View File

@@ -1,4 +0,0 @@
export interface ILogQLParsedQueryItem {
type: string;
value: string | string[];
}

View File

@@ -35,12 +35,6 @@ function renderSelector(
);
}
async function openDropdown(): Promise<void> {
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const control = screen.getByTestId('variable-select-env');
await user.click(control.querySelector('input') as HTMLInputElement);
}
/** Hovers an element and lets the tooltip's open delay elapse. */
async function hover(element: HTMLElement): Promise<void> {
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
@@ -118,95 +112,17 @@ describe('ValueSelector', () => {
});
});
describe('a dynamic variable', () => {
function renderDynamic(
complete: boolean,
relatedValues: string[],
): jest.Mock {
const onSearch = jest.fn();
render(
<TooltipProvider>
<ValueSelector
options={OPTIONS}
variableType="dynamic"
multiSelect
showAllOption
selection={{ value: [], allSelected: false }}
onChange={jest.fn()}
emptyFallback={{ value: [], allSelected: false }}
testId="variable-select-env"
dynamic={{
values: OPTIONS,
relatedValues,
complete,
onSearch,
onSearchReset: jest.fn(),
}}
/>
</TooltipProvider>,
);
return onSearch;
}
it('splits related values out of the full list', async () => {
renderDynamic(true, ['checkout-service-prod']);
await openDropdown();
expect(
screen.getByRole('heading', { level: 2, name: /Related Values/ }),
).toBeInTheDocument();
expect(
screen.getByRole('heading', { level: 2, name: /All Values/ }),
).toBeInTheDocument();
});
it('still opens its dropdown in single-select', async () => {
// The shared single select spreads unknown props over its own handlers, so
// passing it an `onDropdownVisibleChange` silently kills its open state.
render(
<TooltipProvider>
<ValueSelector
options={OPTIONS}
variableType="dynamic"
multiSelect={false}
showAllOption={false}
selection={{ value: '', allSelected: false }}
onChange={jest.fn()}
emptyFallback={{ value: '', allSelected: false }}
testId="variable-select-env"
dynamic={{
values: OPTIONS,
relatedValues: [],
complete: false,
onSearch: jest.fn(),
onSearchReset: jest.fn(),
}}
/>
</TooltipProvider>,
);
await openDropdown();
expect(screen.getByText('cart-service-prod')).toBeInTheDocument();
});
it('routes typing to the API search when the list is truncated', async () => {
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const onSearch = renderDynamic(false, []);
await openDropdown();
await user.keyboard('pay');
expect(onSearch).toHaveBeenLastCalledWith('pay');
});
});
describe('clearing', () => {
function clearIcon(): Element | null {
return document.querySelector('.ant-select-clear');
}
async function openDropdown(): Promise<void> {
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const control = screen.getByTestId('variable-select-env');
await user.click(control.querySelector('input') as HTMLInputElement);
}
it('offers no clear icon while the list is closed', () => {
renderSelector({ value: VALUES, allSelected: false }, OPTIONS);

View File

@@ -114,149 +114,4 @@ describe('useFetchedVariableOptions', () => {
await waitFor(() => expect(result.current.options).toStrictEqual(['prod']));
});
it('keeps related values as their own section and as selectable options', async () => {
mockGetFieldValues.mockResolvedValue({
data: {
normalizedValues: ['cart', 'payments'],
relatedValues: ['checkout'],
complete: true,
},
});
useDashboardStore.setState({
variableFetchStates: { env: VariableFetchState.Loading },
variableCycleIds: { env: 1 },
});
const variable = dynamicVariable('env');
const { result } = renderHook(
() => useFetchedVariableOptions(variable, [variable], {}),
{ wrapper },
);
await waitFor(() =>
expect(result.current.dynamic?.relatedValues).toStrictEqual(['checkout']),
);
expect(result.current.dynamic?.values).toStrictEqual(['cart', 'payments']);
// A related value the unscoped list never returned is still selectable.
expect(result.current.options).toStrictEqual([
'cart',
'payments',
'checkout',
]);
});
it('sends the search to the API when the list is incomplete', async () => {
mockGetFieldValues.mockImplementation((_signal, _name, searchText) =>
Promise.resolve({
data: searchText
? { normalizedValues: ['payments'], relatedValues: [], complete: false }
: { normalizedValues: ['cart'], relatedValues: [], complete: false },
}),
);
useDashboardStore.setState({
variableFetchStates: { env: VariableFetchState.Loading },
variableCycleIds: { env: 1 },
});
const variable = dynamicVariable('env');
const { result } = renderHook(
() => useFetchedVariableOptions(variable, [variable], {}),
{ wrapper },
);
await waitFor(() =>
expect(result.current.dynamic?.values).toStrictEqual(['cart']),
);
act(() => {
result.current.dynamic?.onSearch('pay');
});
await waitFor(() =>
expect(result.current.dynamic?.values).toStrictEqual(['payments']),
);
expect(mockGetFieldValues).toHaveBeenCalledWith(
undefined,
'service.name',
'pay',
1_000,
2_000,
undefined,
expect.anything(),
);
// The search narrows the dropdown only — the selectable set is the full list,
// so a pick made before searching is never reconciled away.
expect(result.current.options).toStrictEqual(['cart']);
// Clearing falls straight back to the base fetch's options — synchronously, so
// closing the dropdown cannot leave the last search's results on screen for a
// debounce interval. They come from the cache of a separate query the search
// never touched, so nothing is refetched.
act(() => {
result.current.dynamic?.onSearchReset();
});
expect(result.current.dynamic?.values).toStrictEqual(['cart']);
expect(mockGetFieldValues).toHaveBeenCalledTimes(2);
});
it('marks a client error as not retryable', async () => {
mockGetFieldValues.mockRejectedValue(
Object.assign(new Error('bad request'), { response: { status: 400 } }),
);
useDashboardStore.setState({
variableFetchStates: { env: VariableFetchState.Loading },
variableCycleIds: { env: 1 },
});
const variable = dynamicVariable('env');
const { result } = renderHook(
() => useFetchedVariableOptions(variable, [variable], {}),
{ wrapper },
);
await waitFor(() => expect(result.current.isRetryable).toBe(false));
});
it('scopes the fetch by a sibling dynamic selection, skipping ALL', async () => {
mockGetFieldValues.mockResolvedValue(fieldValues(['cart']));
useDashboardStore.setState({
variableFetchStates: { env: VariableFetchState.Loading },
variableCycleIds: { env: 1 },
});
const env = dynamicVariable('env');
const namespace: VariableFormModel = {
...dynamicVariable('namespace'),
dynamicAttribute: 'k8s.namespace.name',
};
const region: VariableFormModel = {
...dynamicVariable('region'),
dynamicAttribute: 'cloud.region',
};
renderHook(
() =>
useFetchedVariableOptions(env, [env, namespace, region], {
namespace: { value: ['prod'], allSelected: false },
// ALL means "no filter", so it contributes nothing to existingQuery —
// which is why the backend returns no related values for it.
region: { value: null, allSelected: true },
}),
{ wrapper },
);
await waitFor(() =>
expect(mockGetFieldValues).toHaveBeenCalledWith(
undefined,
'service.name',
undefined,
1_000,
2_000,
"k8s.namespace.name = 'prod'",
),
);
});
});

View File

@@ -4,9 +4,7 @@ import { CustomMultiSelect, CustomSelect } from 'components/NewSelect';
import type { OptionData } from 'components/NewSelect/types';
import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
import type { DynamicVariableOptions } from '../../hooks/useFetchedVariableOptions';
import type { VariableSelection } from '../../selectionTypes';
import { dynamicVariableOptions } from '../../utils/dynamicVariableOptions';
import { areSelectionsEqual } from '../../utils/resolveVariableSelection';
import { selectionFromCommittedValues } from '../../utils/selectionUtils';
import OverflowValuesTooltip from './OverflowValuesTooltip';
@@ -26,10 +24,6 @@ interface ValueSelectorProps {
/** Option-fetch error surfaced in the dropdown, with a retry action. */
errorMessage?: string | null;
onRetry?: () => void;
/** Hides the retry action for an error that retrying cannot fix. */
isRetryable?: boolean;
/** DYNAMIC only: sectioned rendering and server-side search. */
dynamic?: DynamicVariableOptions;
}
function ValueSelector({
@@ -44,15 +38,10 @@ function ValueSelector({
testId,
errorMessage,
onRetry,
isRetryable = true,
dynamic,
}: ValueSelectorProps): JSX.Element {
const optionData = useMemo<OptionData[]>(
() =>
dynamic
? dynamicVariableOptions(dynamic.values, dynamic.relatedValues)
: options.map((option) => ({ label: option, value: option })),
[options, dynamic],
() => options.map((option) => ({ label: option, value: option })),
[options],
);
// All-selected → the full option set so CustomMultiSelect engages its "all"
@@ -130,7 +119,6 @@ function ValueSelector({
loading={loading}
errorMessage={errorMessage}
onRetry={onRetry}
showRetryButton={isRetryable}
showSearch
// Clearing belongs to the open list: on the closed control the icon would
// appear on hover, in a row of variable pills, for an action whose result is
@@ -148,11 +136,6 @@ function ValueSelector({
)}
// Offer ALL only once options load, else a concrete value reads as "all".
enableAllSelection={showAllOption && options.length > 0}
isDynamicVariable={!!dynamic}
onSearch={dynamic?.onSearch}
showIncompleteDataMessage={
!!dynamic && !dynamic.complete && dynamic.values.length > 0
}
onDropdownVisibleChange={(open): void => {
if (open) {
setDraft(committedValues);
@@ -161,7 +144,6 @@ function ValueSelector({
}
setIsOpen(false);
dynamic?.onSearchReset();
commit(draft);
}}
onChange={(next): void => {
@@ -198,14 +180,8 @@ function ValueSelector({
loading={loading}
errorMessage={errorMessage}
onRetry={onRetry}
showRetryButton={isRetryable}
showSearch
placeholder="Select value"
isDynamicVariable={!!dynamic}
onSearch={dynamic?.onSearch}
showIncompleteDataMessage={
!!dynamic && !dynamic.complete && dynamic.values.length > 0
}
onChange={(next): void => {
void logEvent(
DashboardDetailEvents.VariableValueSelected,

View File

@@ -42,8 +42,11 @@ function VariableValueControl({
onChange,
onAutoSelect,
}: VariableValueControlProps): JSX.Element {
const { options, loading, errorMessage, onRetry, isRetryable, dynamic } =
useVariableOptions(variable, variables, selections);
const { options, loading, errorMessage, onRetry } = useVariableOptions(
variable,
variables,
selections,
);
useAutoSelect(variable, options, selection, onAutoSelect);
@@ -62,8 +65,6 @@ function VariableValueControl({
loading={loading}
errorMessage={errorMessage}
onRetry={onRetry}
isRetryable={isRetryable}
dynamic={dynamic}
selection={selection}
onChange={onChange}
emptyFallback={emptyFallback}

View File

@@ -1,89 +0,0 @@
import { useCallback, useState } from 'react';
import { useQuery } from 'react-query';
import { getFieldValues } from 'api/dynamicVariables/getFieldValues';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import useDebounce from 'hooks/useDebounce';
interface UseDynamicVariableSearchProps {
signal?: 'traces' | 'logs' | 'metrics';
attribute?: string;
startUnixMilli: number;
endUnixMilli: number;
existingQuery?: string;
/** Only a truncated list needs the API — a complete one is filtered in the dropdown. */
enabled: boolean;
}
export interface DynamicVariableSearch {
/** Results while a server search is in effect, else null — render the base options. */
results: { values: string[]; relatedValues: string[] } | null;
isSearching: boolean;
onSearch: (text: string) => void;
reset: () => void;
}
/**
* Server-side value search for a DYNAMIC variable, deliberately kept off the fetch
* engine's own query: a keystroke must not settle the variable's fetch cycle and
* re-cascade its dependent variables and panels.
*/
export function useDynamicVariableSearch({
signal,
attribute,
startUnixMilli,
endUnixMilli,
existingQuery,
enabled,
}: UseDynamicVariableSearchProps): DynamicVariableSearch {
const [searchText, setSearchText] = useState('');
const debouncedSearchText = useDebounce(searchText, DEBOUNCE_DELAY);
const isActive =
enabled && !!attribute && !!searchText && !!debouncedSearchText;
const { data, isFetching } = useQuery(
[
'dashboard-variable-dynamic-search',
signal,
attribute,
debouncedSearchText,
existingQuery,
startUnixMilli,
endUnixMilli,
],
({ signal: abortSignal }) =>
getFieldValues(
signal,
attribute,
debouncedSearchText,
startUnixMilli,
endUnixMilli,
existingQuery,
abortSignal,
),
{ enabled: isActive, refetchOnWindowFocus: false, keepPreviousData: true },
);
const reset = useCallback((): void => setSearchText(''), []);
// No results yet falls back to the base options rather than an empty dropdown:
// the select filters them locally, so the list narrows while the API answers.
const results = isActive ? data?.data : undefined;
if (!results) {
return {
results: null,
isSearching: isActive && isFetching,
onSearch: setSearchText,
reset,
};
}
return {
results: {
values: results.normalizedValues ?? [],
relatedValues: results.relatedValues ?? [],
},
isSearching: isFetching,
onSearch: setSearchText,
reset,
};
}

View File

@@ -9,7 +9,6 @@ import {
DASHBOARD_CACHE_TIME_ON_REFRESH_ENABLED,
} from 'constants/queryCacheTime';
import type { AppState } from 'store/reducers';
import { isRetryableError } from 'utils/errorUtils';
import type { GlobalReducer } from 'types/reducer/globalTime';
import {
@@ -21,29 +20,13 @@ import { useDashboardStore } from '../../store/useDashboardStore';
import { buildExistingDynamicVariableQuery } from '../utils/dynamicFilter';
import type { VariableSelectionMap } from '../selectionTypes';
import { selectionToPayload } from '../utils/selectionUtils';
import { useDynamicVariableSearch } from './useDynamicVariableSearch';
import { useVariableFetchState } from './useVariableFetchState';
export interface DynamicVariableOptions {
/** ALL VALUES section — narrowed to the API's matches while a search is active. */
values: string[];
/** RELATED VALUES section — scoped by the sibling dynamic variables' selections. */
relatedValues: string[];
/** false when the backend truncated the list, so searching has to hit the API. */
complete: boolean;
onSearch: (text: string) => void;
onSearchReset: () => void;
}
export interface VariableOptions {
options: string[];
loading: boolean;
errorMessage: string | null;
onRetry?: () => void;
/** false for a client error, where retrying the same request cannot help. */
isRetryable?: boolean;
/** DYNAMIC only: what the dropdown renders, sectioned and search-aware. */
dynamic?: DynamicVariableOptions;
}
/**
@@ -167,68 +150,10 @@ export function useFetchedVariableOptions(
return sortValuesByOrder(values, variable.sort).map(String);
}, [dynamicResult.data, variable.sort]);
const dynamicRelatedOptions = useMemo(
() =>
sortValuesByOrder(
dynamicResult.data?.data?.relatedValues ?? [],
variable.sort,
).map(String),
[dynamicResult.data, variable.sort],
);
// Related values are scoped by the sibling selections, so they can name values the
// unscoped list never returned — the selectable set is the union of both sections.
const dynamicSelectableOptions = useMemo(
() => [...new Set([...dynamicOptions, ...dynamicRelatedOptions])],
[dynamicOptions, dynamicRelatedOptions],
);
const isDynamicListComplete = dynamicResult.data?.data?.complete ?? true;
const search = useDynamicVariableSearch({
signal: signalForApi(variable.dynamicSignal),
attribute: variable.dynamicAttribute,
startUnixMilli: minTime,
endUnixMilli: maxTime,
existingQuery: existingQuery || undefined,
enabled: variable.type === 'DYNAMIC' && !isDynamicListComplete,
});
// One stable object: the select rebuilds its whole option list whenever this
// identity changes, so it must not be a literal rebuilt on every render.
const dynamicDisplay = useMemo<DynamicVariableOptions>(() => {
const display = search.results
? {
values: sortValuesByOrder(search.results.values, variable.sort).map(
String,
),
relatedValues: sortValuesByOrder(
search.results.relatedValues,
variable.sort,
).map(String),
}
: { values: dynamicOptions, relatedValues: dynamicRelatedOptions };
return {
...display,
complete: isDynamicListComplete,
onSearch: search.onSearch,
onSearchReset: search.reset,
};
}, [
search.results,
search.onSearch,
search.reset,
isDynamicListComplete,
dynamicOptions,
dynamicRelatedOptions,
variable.sort,
]);
// Flag a variable that settled with zero options so dependent panels fall through
// to "no data" instead of waiting forever. hasFetchedOnce excludes the pre-fetch state.
const effectiveOptions =
variable.type === 'DYNAMIC' ? dynamicSelectableOptions : queryOptions;
variable.type === 'DYNAMIC' ? dynamicOptions : queryOptions;
useEffect(() => {
if (variable.type !== 'QUERY' && variable.type !== 'DYNAMIC') {
return;
@@ -250,16 +175,14 @@ export function useFetchedVariableOptions(
if (variable.type === 'DYNAMIC') {
return {
options: dynamicSelectableOptions,
loading: dynamicResult.isFetching || isVariableWaiting || search.isSearching,
options: dynamicOptions,
loading: dynamicResult.isFetching || isVariableWaiting,
errorMessage: dynamicResult.error
? (dynamicResult.error as Error).message || null
: null,
onRetry: (): void => {
void dynamicResult.refetch();
},
isRetryable: !dynamicResult.error || isRetryableError(dynamicResult.error),
dynamic: dynamicDisplay,
};
}
return {
@@ -271,6 +194,5 @@ export function useFetchedVariableOptions(
onRetry: (): void => {
void queryResult.refetch();
},
isRetryable: !queryResult.error || isRetryableError(queryResult.error),
};
}

View File

@@ -1,27 +0,0 @@
import type { OptionData } from 'components/NewSelect/types';
const toOptions = (values: string[]): OptionData[] =>
values.map((value) => ({ label: value, value }));
/**
* Dropdown options for a DYNAMIC variable: values scoped by the other dynamic
* variables' selections get their own section above the unscoped list. Without
* related values there is nothing to contrast, so the list stays flat.
*/
export function dynamicVariableOptions(
values: string[],
relatedValues: string[],
): OptionData[] {
if (relatedValues.length === 0) {
return toOptions(values);
}
return [
{
label: 'Related Values',
value: 'relatedValues',
options: toOptions(relatedValues),
},
{ label: 'All Values', value: 'allValues', options: toOptions(values) },
];
}

View File

@@ -0,0 +1,26 @@
.ai-observability-module-container {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
.ant-tabs {
height: 100%;
}
.ant-tabs-nav {
padding: var(--spacing-0) var(--spacing-8);
margin-bottom: var(--spacing-0);
&::before {
border-bottom: 1px solid var(--l1-border) !important;
}
}
.tab-item {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-4);
}
}

View File

@@ -1,14 +1,14 @@
import { safeNavigateMock } from '__tests__/safeNavigateMock';
import ROUTES from 'constants/routes';
import {
LLM_PRICING_ENDPOINT,
makeListResponse,
mockRules,
} from 'container/LLMObservability/Settings/ModelPricing/__tests__/fixtures';
import history from 'lib/history';
import { rest, server } from 'mocks-server/server';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import LLMObservability from '../LLMObservability';
import LLMObservabilityPage from '..';
// The Overview tab renders the full V2 DashboardContainer (toolbar + date picker
// call useNavigationType, which needs a data router this integration test doesn't
@@ -24,6 +24,16 @@ jest.mock('container/LLMObservability/Explorer/Explorer', () => ({
default: (): JSX.Element => <div data-testid="llm-observability-explorer" />,
}));
jest.mock('lib/history', () => ({
push: jest.fn(),
listen: jest.fn(() => jest.fn()),
location: { pathname: '/ai-observability/overview', search: '' },
}));
const mockHistoryPush = history.push as jest.MockedFunction<
typeof history.push
>;
const SYSTEM_DASHBOARD_ENDPOINT = '*/api/v2/dashboards/system/ai-o11y-overview';
function setupSystemDashboard(): void {
@@ -64,6 +74,7 @@ function setupList(items = mockRules): void {
describe('LLMObservability (integration)', () => {
beforeEach(() => {
window.history.pushState(null, '', '/');
mockHistoryPush.mockClear();
});
afterEach(() => {
@@ -72,11 +83,11 @@ describe('LLMObservability (integration)', () => {
it('renders the overview panel and the tab bar on the overview route', async () => {
setupSystemDashboard();
render(<LLMObservability />, undefined, {
render(<LLMObservabilityPage />, undefined, {
initialRoute: ROUTES.AI_OBSERVABILITY_OVERVIEW,
});
expect(screen.getByTestId('llm-observability-tabs')).toBeInTheDocument();
expect(screen.getByTestId('llm-observability-page')).toBeInTheDocument();
expect(screen.getByTestId('llm-observability-overview')).toBeInTheDocument();
await waitFor(() =>
expect(screen.getByTestId('llm-overview-dashboard')).toBeInTheDocument(),
@@ -84,54 +95,54 @@ describe('LLMObservability (integration)', () => {
expect(screen.getByRole('tab', { name: 'Overview' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Explorer' })).toBeInTheDocument();
expect(
screen.getByRole('tab', { name: 'Model pricing' }),
screen.getByRole('tab', { name: /Model pricing/ }),
).toBeInTheDocument();
expect(
screen.getByRole('tab', { name: 'Attribute Mapping' }),
screen.getByRole('tab', { name: /Attribute Mapping/ }),
).toBeInTheDocument();
});
it('navigates to the configuration route when the Model pricing tab is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<LLMObservability />, undefined, {
render(<LLMObservabilityPage />, undefined, {
initialRoute: ROUTES.AI_OBSERVABILITY_OVERVIEW,
});
await user.click(screen.getByRole('tab', { name: 'Model pricing' }));
await user.click(screen.getByRole('tab', { name: /Model pricing/ }));
expect(safeNavigateMock).toHaveBeenCalledWith(
expect(mockHistoryPush).toHaveBeenCalledWith(
ROUTES.AI_OBSERVABILITY_CONFIGURATION,
);
});
it('navigates to the attribute mapping route when that tab is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<LLMObservability />, undefined, {
render(<LLMObservabilityPage />, undefined, {
initialRoute: ROUTES.AI_OBSERVABILITY_OVERVIEW,
});
await user.click(screen.getByRole('tab', { name: 'Attribute Mapping' }));
await user.click(screen.getByRole('tab', { name: /Attribute Mapping/ }));
expect(safeNavigateMock).toHaveBeenCalledWith(
expect(mockHistoryPush).toHaveBeenCalledWith(
ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING,
);
});
it('navigates to the explorer route when the Explorer tab is clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<LLMObservability />, undefined, {
render(<LLMObservabilityPage />, undefined, {
initialRoute: ROUTES.AI_OBSERVABILITY_OVERVIEW,
});
await user.click(screen.getByRole('tab', { name: 'Explorer' }));
await user.click(screen.getByRole('tab', { name: /Explorer/ }));
expect(safeNavigateMock).toHaveBeenCalledWith(
expect(mockHistoryPush).toHaveBeenCalledWith(
ROUTES.AI_OBSERVABILITY_EXPLORER,
);
});
it('renders the explorer panel on the explorer route', () => {
render(<LLMObservability />, undefined, {
render(<LLMObservabilityPage />, undefined, {
initialRoute: ROUTES.AI_OBSERVABILITY_EXPLORER,
});
@@ -139,7 +150,7 @@ describe('LLMObservability (integration)', () => {
});
it('renders the attribute mapping page on the attribute mapping route', () => {
render(<LLMObservability />, undefined, {
render(<LLMObservabilityPage />, undefined, {
initialRoute: ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING,
});
@@ -150,7 +161,7 @@ describe('LLMObservability (integration)', () => {
it('renders the model-pricing page on the configuration route', async () => {
setupList();
render(<LLMObservability />, undefined, {
render(<LLMObservabilityPage />, undefined, {
initialRoute: ROUTES.AI_OBSERVABILITY_CONFIGURATION,
});

View File

@@ -0,0 +1,56 @@
import { TabRoutes } from 'components/RouteTab/types';
import ROUTES from 'constants/routes';
import LLMObservabilityAttributeMapping from 'container/LLMObservability/AttributeMapping/LLMObservabilityAttributeMapping';
import ExplorerPage from 'container/LLMObservability/Explorer/Explorer';
import OverviewPage from 'container/LLMObservability/Overview/Overview';
import LLMObservabilityModelPricing from 'container/LLMObservability/Settings/ModelPricing/LLMObservabilityModelPricing';
import {
ArrowRightLeft,
BarChart,
CircleDollarSign,
Compass,
} from '@signozhq/icons';
export const Overview: TabRoutes = {
Component: OverviewPage,
name: (
<div className="tab-item">
<BarChart size={16} /> Overview
</div>
),
route: ROUTES.AI_OBSERVABILITY_OVERVIEW,
key: ROUTES.AI_OBSERVABILITY_OVERVIEW,
};
export const Explorer: TabRoutes = {
Component: ExplorerPage,
name: (
<div className="tab-item">
<Compass size={16} /> Explorer
</div>
),
route: ROUTES.AI_OBSERVABILITY_EXPLORER,
key: ROUTES.AI_OBSERVABILITY_EXPLORER,
};
export const ModelPricing: TabRoutes = {
Component: LLMObservabilityModelPricing,
name: (
<div className="tab-item">
<CircleDollarSign size={16} /> Model pricing
</div>
),
route: ROUTES.AI_OBSERVABILITY_CONFIGURATION,
key: ROUTES.AI_OBSERVABILITY_CONFIGURATION,
};
export const AttributeMapping: TabRoutes = {
Component: LLMObservabilityAttributeMapping,
name: (
<div className="tab-item">
<ArrowRightLeft size={16} /> Attribute Mapping
</div>
),
route: ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING,
key: ROUTES.AI_OBSERVABILITY_ATTRIBUTE_MAPPING,
};

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