Compare commits

..

1 Commits

Author SHA1 Message Date
Gaurav Tewari
e5885d2856 fix(uplot): stop the time-scale trim hiding data on short windows
Assisted-by: Claude Opus 5
2026-08-12 16:06:46 +05:30
163 changed files with 13024 additions and 4322 deletions

View File

@@ -61,7 +61,6 @@ jobs:
- querierauthz
- role
- rootuser
- savedview
- serviceaccount
- spanmapper
- querier_json_body

View File

@@ -7880,20 +7880,17 @@ components:
type: string
SavedviewtypesPostableSavedView:
properties:
data:
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
generateName:
type: boolean
name:
type: string
schemaVersion:
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
source:
$ref: '#/components/schemas/SavedviewtypesSource'
spec:
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
required:
- source
- schemaVersion
- spec
- data
type: object
SavedviewtypesSavedView:
properties:
@@ -7902,16 +7899,14 @@ components:
type: string
createdBy:
type: string
data:
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
id:
type: string
name:
type: string
schemaVersion:
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
source:
$ref: '#/components/schemas/SavedviewtypesSource'
spec:
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
updatedAt:
format: date-time
type: string
@@ -7919,6 +7914,14 @@ components:
type: string
required:
- id
type: object
SavedviewtypesSavedViewData:
properties:
schemaVersion:
type: string
spec:
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
required:
- schemaVersion
- spec
type: object
@@ -7933,10 +7936,7 @@ components:
queries:
items:
$ref: '#/components/schemas/Querybuildertypesv5QueryEnvelope'
minItems: 1
type: array
requestType:
$ref: '#/components/schemas/Querybuildertypesv5RequestType'
selectedFields:
items:
$ref: '#/components/schemas/TelemetrytypesTelemetryFieldKey'
@@ -7944,13 +7944,10 @@ components:
required:
- displayName
- panelType
- requestType
- queries
- selectedFields
- display
type: object
SavedviewtypesSchemaVersion:
enum:
- v2
type: string
SavedviewtypesSource:
enum:
- traces
@@ -7960,16 +7957,13 @@ components:
type: string
SavedviewtypesUpdatableSavedView:
properties:
schemaVersion:
$ref: '#/components/schemas/SavedviewtypesSchemaVersion'
data:
$ref: '#/components/schemas/SavedviewtypesSavedViewData'
source:
$ref: '#/components/schemas/SavedviewtypesSource'
spec:
$ref: '#/components/schemas/SavedviewtypesSavedViewSpec'
required:
- source
- schemaVersion
- spec
- data
type: object
ServiceaccounttypesDeprecatedPostableServiceAccountRole:
properties:
@@ -22782,12 +22776,6 @@ paths:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Forbidden
"409":
content:
application/json:
schema:
$ref: '#/components/schemas/RenderErrorResponse'
description: Conflict
"500":
content:
application/json:

View File

@@ -98,6 +98,14 @@ func (ah *APIHandler) getFeatureFlags(w http.ResponseWriter, r *http.Request) {
Route: "",
})
if constants.IsDotMetricsEnabled {
for idx, feature := range featureSet {
if feature.Name == licensetypes.DotMetricsEnabled {
featureSet[idx].Active = true
}
}
}
ah.Respond(w, featureSet)
}

View File

@@ -17,3 +17,15 @@ func GetOrDefaultEnv(key string, fallback string) string {
}
return v
}
// constant functions that override env vars
const DotMetricsEnabled = "DOT_METRICS_ENABLED"
var IsDotMetricsEnabled = false
func init() {
if GetOrDefaultEnv(DotMetricsEnabled, "true") == "true" {
IsDotMetricsEnabled = true
}
}

View File

@@ -376,23 +376,7 @@ function App(): JSX.Element {
tracesSampleRate: 0, // Ref: https://github.com/SigNoz/platform-pod/issues/2393#issuecomment-4603658055
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
beforeSend(event, hint) {
const error = hint?.originalException as
| { name?: string; code?: string | number }
| undefined;
// Ignore benign aborted/cancelled requests (axios + fetch).
if (error?.code === 'ERR_CANCELED' || error?.code === 'ECONNABORTED') {
return null;
}
if (error?.name === 'AbortError') {
return null;
}
// Ignore benign Monaco cancellation errors (name 'Canceled').
if (error?.name === 'Canceled') {
return null;
}
beforeSend(event) {
// Drop the event if its level is 'warning' or 'info'
if (event.level === 'warning' || event.level === 'info') {
return null;

View File

@@ -8991,17 +8991,8 @@ export enum SavedviewtypesPanelTypeDTO {
list = 'list',
trace = 'trace',
}
export enum SavedviewtypesSchemaVersionDTO {
v2 = 'v2',
}
export enum SavedviewtypesSourceDTO {
traces = 'traces',
logs = 'logs',
metrics = 'metrics',
meter = 'meter',
}
export interface SavedviewtypesSavedViewSpecDTO {
display?: SavedviewtypesDisplayDTO;
display: SavedviewtypesDisplayDTO;
/**
* @type string
*/
@@ -9011,14 +9002,28 @@ export interface SavedviewtypesSavedViewSpecDTO {
* @type array
*/
queries: Querybuildertypesv5QueryEnvelopeDTO[];
requestType: Querybuildertypesv5RequestTypeDTO;
/**
* @type array
*/
selectedFields?: TelemetrytypesTelemetryFieldKeyDTO[];
selectedFields: TelemetrytypesTelemetryFieldKeyDTO[];
}
export interface SavedviewtypesSavedViewDataDTO {
/**
* @type string
*/
schemaVersion: string;
spec: SavedviewtypesSavedViewSpecDTO;
}
export enum SavedviewtypesSourceDTO {
traces = 'traces',
logs = 'logs',
metrics = 'metrics',
meter = 'meter',
}
export interface SavedviewtypesPostableSavedViewDTO {
data: SavedviewtypesSavedViewDataDTO;
/**
* @type boolean
*/
@@ -9027,9 +9032,7 @@ export interface SavedviewtypesPostableSavedViewDTO {
* @type string
*/
name?: string;
schemaVersion: SavedviewtypesSchemaVersionDTO;
source: SavedviewtypesSourceDTO;
spec: SavedviewtypesSavedViewSpecDTO;
}
export interface SavedviewtypesSavedViewDTO {
@@ -9042,6 +9045,7 @@ export interface SavedviewtypesSavedViewDTO {
* @type string
*/
createdBy?: string;
data?: SavedviewtypesSavedViewDataDTO;
/**
* @type string
*/
@@ -9050,9 +9054,7 @@ export interface SavedviewtypesSavedViewDTO {
* @type string
*/
name?: string;
schemaVersion: SavedviewtypesSchemaVersionDTO;
source?: SavedviewtypesSourceDTO;
spec: SavedviewtypesSavedViewSpecDTO;
/**
* @type string
* @format date-time
@@ -9065,9 +9067,8 @@ export interface SavedviewtypesSavedViewDTO {
}
export interface SavedviewtypesUpdatableSavedViewDTO {
schemaVersion: SavedviewtypesSchemaVersionDTO;
data: SavedviewtypesSavedViewDataDTO;
source: SavedviewtypesSourceDTO;
spec: SavedviewtypesSavedViewSpecDTO;
}
export interface ServiceaccounttypesDeprecatedPostableServiceAccountRoleDTO {

View File

@@ -24,3 +24,19 @@ export const Logout = async (): Promise<void> => {
window.dispatchEvent(new CustomEvent('LOGOUT'));
history.push(ROUTES.LOGIN);
};
export const UnderscoreToDotMap: Record<string, string> = {
k8s_cluster_name: 'k8s.cluster.name',
k8s_cluster_uid: 'k8s.cluster.uid',
k8s_namespace_name: 'k8s.namespace.name',
k8s_node_name: 'k8s.node.name',
k8s_node_uid: 'k8s.node.uid',
k8s_pod_name: 'k8s.pod.name',
k8s_pod_uid: 'k8s.pod.uid',
k8s_deployment_name: 'k8s.deployment.name',
k8s_daemonset_name: 'k8s.daemonset.name',
k8s_statefulset_name: 'k8s.statefulset.name',
k8s_cronjob_name: 'k8s.cronjob.name',
k8s_job_name: 'k8s.job.name',
k8s_persistentvolumeclaim_name: 'k8s.persistentvolumeclaim.name',
};

View File

@@ -0,0 +1,31 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/user/resetPassword';
/**
* @deprecated Use the generated `useResetPassword` hook (or `resetPassword` fetcher) from
* `api/generated/services/users` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const resetPassword = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.post<PayloadProps>(`/resetPassword`, {
...props,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default resetPassword;

View File

@@ -0,0 +1,31 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { PayloadProps, Props } from 'types/api/user/setInvite';
/**
* @deprecated Use the generated `useCreateInvite` hook (or `createInvite` fetcher) from
* `api/generated/services/users` instead. This hand-written client targets the
* same endpoint and will be removed once call sites migrate.
*
* Part of https://github.com/SigNoz/engineering-pod/issues/5289, add a comment or update when removing this method.
*/
const sendInvite = async (
props: Props,
): Promise<SuccessResponseV2<PayloadProps>> => {
try {
const response = await axios.post<PayloadProps>(`/invite`, {
...props,
});
return {
httpStatusCode: response.status,
data: response.data,
};
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}
};
export default sendInvite;

View File

@@ -5,9 +5,10 @@ import {
useCreateResetPasswordToken,
useDeleteUser,
useGetResetPasswordToken,
useCreateUserRole,
useDeleteUserRole,
useGetRolesByUserID,
useGetUser,
useRemoveUserRoleByUserIDAndRoleID,
useSetRoleByUserID,
useUpdateMyUserV2,
useUpdateUser,
} from 'api/generated/services/users';
@@ -24,14 +25,15 @@ import EditMemberDrawer, { EditMemberDrawerProps } from '../EditMemberDrawer';
jest.mock('api/generated/services/users', () => ({
useDeleteUser: jest.fn(),
useGetUser: jest.fn(),
useDeleteUserRole: jest.fn(),
useGetRolesByUserID: jest.fn(),
useRemoveUserRoleByUserIDAndRoleID: jest.fn(),
useUpdateUser: jest.fn(),
useUpdateMyUserV2: jest.fn(),
useCreateUserRole: jest.fn(),
useSetRoleByUserID: jest.fn(),
useGetResetPasswordToken: jest.fn(),
useCreateResetPasswordToken: jest.fn(),
getGetUserQueryKey: ({ id }: { id: string }): string[] => [
`/api/v2/users/${id}`,
getGetRolesByUserIDQueryKey: ({ id }: { id: string }): string[] => [
`/api/v2/users/${id}/roles`,
],
}));
@@ -192,7 +194,11 @@ describe('EditMemberDrawer', () => {
isLoading: false,
refetch: jest.fn(),
});
(useDeleteUserRole as jest.Mock).mockReturnValue({
(useGetRolesByUserID as jest.Mock).mockReturnValue({
data: { data: [managedRoles[0]] },
isLoading: false,
});
(useRemoveUserRoleByUserIDAndRoleID as jest.Mock).mockReturnValue({
mutateAsync: mockRemoveMutateAsync.mockResolvedValue({}),
isLoading: false,
});
@@ -204,7 +210,7 @@ describe('EditMemberDrawer', () => {
mutateAsync: jest.fn().mockResolvedValue({}),
isLoading: false,
});
(useCreateUserRole as jest.Mock).mockReturnValue({
(useSetRoleByUserID as jest.Mock).mockReturnValue({
mutateAsync: jest.fn().mockResolvedValue({}),
isLoading: false,
});
@@ -306,12 +312,12 @@ describe('EditMemberDrawer', () => {
expect(onClose).not.toHaveBeenCalled();
});
it('adding a new role creates a user role without removing existing ones', async () => {
it('adding a new role calls setRole without removing existing ones', async () => {
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
const mockSet = jest.fn().mockResolvedValue({});
(useCreateUserRole as jest.Mock).mockReturnValue({
(useSetRoleByUserID as jest.Mock).mockReturnValue({
mutateAsync: mockSet,
isLoading: false,
});
@@ -328,14 +334,15 @@ describe('EditMemberDrawer', () => {
await waitFor(() => {
expect(mockSet).toHaveBeenCalledWith({
data: { userId: 'user-1', roleId: managedRoles[1].id },
pathParams: { id: 'user-1' },
data: { name: 'signoz-editor' },
});
expect(mockRemoveMutateAsync).not.toHaveBeenCalled();
expect(onComplete).toHaveBeenCalled();
});
});
it('deselecting a role deletes the user role by its assignment id', async () => {
it('deselecting a role calls removeRole with the role id', async () => {
const onComplete = jest.fn();
const user = userEvent.setup({ pointerEventsCheck: 0 });
@@ -354,7 +361,7 @@ describe('EditMemberDrawer', () => {
await waitFor(() => {
expect(mockRemoveMutateAsync).toHaveBeenCalledWith({
pathParams: { id: 'ur-1' },
pathParams: { id: 'user-1', roleId: managedRoles[0].id },
});
expect(onComplete).toHaveBeenCalled();
});

View File

@@ -1,46 +0,0 @@
.highlights {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px 16px;
padding: 12px 0;
// Constrain each KeyValueLabel (the grid items) to its cell.
:global(.key-value-label) {
width: auto;
min-width: 0;
overflow: hidden;
}
}
.valueBadge {
--badge-font-size: 13px;
box-sizing: border-box;
max-width: 100%;
min-width: 0;
}
// Truncating text inside a badge
.badgeText {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.serviceDot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--accent-forest);
flex-shrink: 0;
margin-right: 4px;
}
.traceLink {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--accent-primary);
}

View File

@@ -1,36 +0,0 @@
import KeyValueLabel from 'periscope/components/KeyValueLabel';
import { ILog } from 'types/api/logs/log';
import { LOG_HIGHLIGHTS } from './config';
import styles from './LogHighlights.module.scss';
interface LogHighlightsProps {
log: ILog;
}
function LogHighlights({ log }: LogHighlightsProps): JSX.Element | null {
const fields = LOG_HIGHLIGHTS.map((field) => ({
key: field.key,
label: field.label,
value: field.render(log),
})).filter((field) => field.value != null);
if (fields.length === 0) {
return null;
}
return (
<div className={styles.highlights} data-testid="log-details-highlights">
{fields.map((field) => (
<KeyValueLabel
key={field.key}
badgeKey={field.label}
badgeValue={field.value}
direction="column"
/>
))}
</div>
);
}
export default LogHighlights;

View File

@@ -1,23 +0,0 @@
import { Link } from 'react-router-dom';
import styles from './LogHighlights.module.scss';
interface TraceIdFieldProps {
traceId: string;
}
function TraceIdField({ traceId }: TraceIdFieldProps): JSX.Element {
return (
<Link
to={{ pathname: `/trace/${traceId}` }}
target="_blank"
rel="noreferrer"
className={styles.traceLink}
title={traceId}
>
{traceId}
</Link>
);
}
export default TraceIdField;

View File

@@ -1,102 +0,0 @@
import { ReactNode } from 'react';
import { Badge, BadgeColor } from '@signozhq/ui/badge';
import { LogType } from 'components/Logs/LogStateIndicator/LogStateIndicator';
import { getLogIndicatorType } from 'components/Logs/LogStateIndicator/utils';
import { ILog } from 'types/api/logs/log';
import styles from './LogHighlights.module.scss';
import TraceIdField from './TraceIdField';
// Severity badge color mirrors the LogStateIndicator bar
const SEVERITY_COLOR: Record<string, BadgeColor> = {
[LogType.TRACE]: 'forest',
[LogType.DEBUG]: 'aqua',
[LogType.INFO]: 'robin',
[LogType.WARN]: 'amber',
[LogType.ERROR]: 'cherry',
[LogType.FATAL]: 'sakura',
};
export interface LogHighlightConfig {
key: string;
label: string;
render: (log: ILog) => ReactNode | null;
}
// Resource/attribute lookup (keys like `service.name` live in resources_string,
// occasionally attributes_string). Typed loosely as these are string maps.
const getAttr = (log: ILog, key: string): string =>
(log.resources_string as unknown as Record<string, string>)?.[key] ||
(log.attributes_string as unknown as Record<string, string>)?.[key] ||
'';
const valueBadge = (
value: string,
options?: { prefix?: ReactNode; color?: BadgeColor },
): ReactNode => (
<Badge color={options?.color ?? 'vanilla'} className={styles.valueBadge}>
{options?.prefix}
<span className={styles.badgeText} title={value}>
{value}
</span>
</Badge>
);
export const LOG_HIGHLIGHTS: LogHighlightConfig[] = [
{
key: 'service',
label: 'SERVICE',
render: (log): ReactNode | null => {
const value = getAttr(log, 'service.name');
return value
? valueBadge(value, {
prefix: <span className={styles.serviceDot} />,
})
: null;
},
},
{
key: 'severity',
label: 'SEVERITY',
render: (log): ReactNode | null => {
if (!log.severity_text) {
return null;
}
return valueBadge(log.severity_text, {
color: SEVERITY_COLOR[getLogIndicatorType(log)] ?? 'vanilla',
});
},
},
{
key: 'namespace',
label: 'NAMESPACE',
render: (log): ReactNode | null => {
const value = getAttr(log, 'service.namespace');
return value ? valueBadge(value) : null;
},
},
{
key: 'environment',
label: 'ENVIRONMENT',
render: (log): ReactNode | null => {
const value = getAttr(log, 'deployment.environment');
return value ? valueBadge(value) : null;
},
},
{
key: 'traceId',
label: 'TRACE ID',
render: (log): ReactNode | null => {
const traceId = log.trace_id || log.traceId;
return traceId ? <TraceIdField traceId={traceId} /> : null;
},
},
{
key: 'spanId',
label: 'SPAN ID',
render: (log): ReactNode | null => {
const spanId = log.span_id || log.spanID;
return spanId ? valueBadge(spanId) : null;
},
},
];

View File

@@ -115,45 +115,6 @@ describe('LogDetail drawer — header (isLogDetailsV2)', () => {
expect(screen.queryByText('Open in Explorer')).not.toBeInTheDocument();
});
it('renders Highlights for fields present on the log, omitting absent ones', () => {
const logWithMeta = {
...mockLog,
severity_text: 'ERROR',
trace_id: 'trace-abc',
resources_string: {
'service.name': 'checkout',
'deployment.environment': 'production',
},
} as unknown as ILog;
renderDrawer({ log: logWithMeta });
const highlights = screen.getByTestId('log-details-highlights');
expect(highlights).toHaveTextContent('SEVERITY');
expect(highlights).toHaveTextContent('ERROR');
expect(highlights).toHaveTextContent('SERVICE');
expect(highlights).toHaveTextContent('checkout');
expect(highlights).toHaveTextContent('ENVIRONMENT');
expect(highlights).toHaveTextContent('production');
expect(highlights).toHaveTextContent('TRACE ID');
// Absent fields are omitted (no namespace / span id on this log).
expect(highlights).not.toHaveTextContent('NAMESPACE');
expect(highlights).not.toHaveTextContent('SPAN ID');
});
it('links the trace id highlight to the trace detail in a new tab', () => {
const logWithTrace = {
...mockLog,
trace_id: 'trace-abc',
} as unknown as ILog;
renderDrawer({ log: logWithTrace });
const link = screen.getByRole('link', { name: 'trace-abc' });
expect(link).toHaveAttribute('target', '_blank');
expect(link.getAttribute('href')).toContain('/trace/trace-abc');
});
it('navigates to the next / previous log with the Down / Up arrow keys', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
const logs = [makeLog('log-0'), makeLog('log-1'), makeLog('log-2')];

View File

@@ -55,7 +55,6 @@ import { isLogDetailsV2, RESOURCE_KEYS, VIEW_TYPES, VIEWS } from './constants';
import { LogDetailInnerProps, LogDetailProps } from './LogDetail.interfaces';
import LogDetailsHeader from './LogDetailsHeader/LogDetailsHeader';
import { useLogNavigation } from './LogDetailsHeader/useLogNavigation';
import LogHighlights from './LogHighlights/LogHighlights';
import './LogDetails.styles.scss';
@@ -400,8 +399,6 @@ function LogDetailInner({
<div className="log-overflow-shadow">&nbsp;</div>
</div>
{isLogDetailsV2 && <LogHighlights log={log} />}
<div className="tabs-and-search">
<ToggleGroupSimple
type="single"

View File

@@ -183,14 +183,15 @@ function QuerySearch({
isProgrammaticChangeRef.current = true;
}
const changes = view.state.changes({
from: 0,
to: currentValue.length,
insert: value,
});
view.dispatch({
changes,
selection: { anchor: changes.newLength },
changes: {
from: 0,
to: currentValue.length,
insert: value,
},
selection: {
anchor: value.length,
},
});
},
[],

View File

@@ -301,66 +301,6 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
dispatchSpy.mockRestore();
});
it('does not crash when the expression contains CRLF line breaks (issue #5869)', async () => {
const dispatchSpy = jest.spyOn(EditorView.prototype, 'dispatch');
const onChange = jest.fn() as jest.MockedFunction<(v: string) => void>;
const initialExpression = "service.name = 'frontend'";
// Filtering on a multi-line log value (CRLF) used to throw
// "RangeError: Selection points outside of document".
const crlfExpression = "body CONTAINS 'line1\r\nline2\r\nline3'";
const baseQueryData = {
...initialQueriesMap.logs.builder.queryData[0],
filter: { expression: initialExpression },
};
const { rerender } = render(
<QuerySearch
onChange={onChange}
queryData={baseQueryData}
dataSource={DataSource.LOGS}
/>,
);
await waitFor(
() => {
const editorContent = document.querySelector(
CM_EDITOR_SELECTOR,
) as HTMLElement;
expect(editorContent.textContent || '').toBe(initialExpression);
},
{ timeout: 3000 },
);
rerender(
<QuerySearch
onChange={onChange}
queryData={{ ...baseQueryData, filter: { expression: crlfExpression } }}
dataSource={DataSource.LOGS}
/>,
);
// The programmatic replace dispatched without throwing, and the selection anchor
// stayed within the CRLF-normalized document (the bug set it past the end).
await waitFor(() => {
const spec = dispatchSpy.mock.calls
.map(
(call) =>
call[0] as {
selection?: { anchor?: number };
changes?: { newLength?: number };
},
)
.find((s) => s?.selection?.anchor != null && s?.changes?.newLength != null);
expect(spec).toBeDefined();
expect(spec?.selection?.anchor).toBeLessThanOrEqual(
spec?.changes?.newLength as number,
);
});
dispatchSpy.mockRestore();
});
it('fetches key suggestions for metrics even without aggregateAttribute.key when showFilterSuggestionsWithoutMetric is true', async () => {
const mockedGetKeys = getKeySuggestions as jest.MockedFunction<
typeof getKeySuggestions

View File

@@ -7,6 +7,7 @@ export enum FeatureKeys {
GATEWAY = 'gateway',
PREMIUM_SUPPORT = 'premium_support',
ANOMALY_DETECTION = 'anomaly_detection',
DOT_METRICS_ENABLED = 'dot_metrics_enabled',
USE_JSON_BODY = 'use_json_body',
ENABLE_AI_OBSERVABILITY = 'enable_ai_observability',
ENABLE_METRICS_REDUCTION = 'enable_metrics_reduction',

View File

@@ -437,17 +437,6 @@ describe('Create Alert Channel', () => {
render(<CreateAlertChannels preType={ChannelType.GoogleChat} />);
});
// paste instead of type: a per-keystroke re-render of the whole form
// pushes these tests past the 5s jest timeout on slower CI runners
async function fillField(
user: ReturnType<typeof userEvent.setup>,
testId: string,
value: string,
): Promise<void> {
await user.click(screen.getByTestId(testId));
await user.paste(value);
}
it('Should check if the selected item in the type dropdown has text "Google Chat"', () => {
expect(screen.getByText('Google Chat')).toBeInTheDocument();
});
@@ -474,8 +463,14 @@ describe('Create Alert Channel', () => {
it('Should check if saving with a webhook url outside chat.googleapis.com displays error notification', async () => {
const user = userEvent.setup();
await fillField(user, 'channel-name-textbox', 'gchat-channel');
await fillField(user, 'webhook-url-textbox', 'https://example.com/webhook');
await user.type(
screen.getByTestId('channel-name-textbox'),
'gchat-channel',
);
await user.type(
screen.getByTestId('webhook-url-textbox'),
'https://example.com/webhook',
);
await user.click(screen.getByTestId('save-channel-button'));
@@ -501,8 +496,11 @@ describe('Create Alert Channel', () => {
const user = userEvent.setup();
await fillField(user, 'channel-name-textbox', 'gchat-channel');
await fillField(user, 'webhook-url-textbox', validWebhookUrl);
await user.type(
screen.getByTestId('channel-name-textbox'),
'gchat-channel',
);
await user.type(screen.getByTestId('webhook-url-textbox'), validWebhookUrl);
await user.click(screen.getByTestId('save-channel-button'));

View File

@@ -37,6 +37,8 @@ import { ErrorResponse, SuccessResponse } from 'types/api';
import { Exception, PayloadProps } from 'types/api/errors/getAll';
import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../constants/features';
import { useAppContext } from '../../providers/App/App';
import { FilterDropdownExtendsProps } from './types';
import {
extractFilterValues,
@@ -416,6 +418,11 @@ function AllErrors(): JSX.Element {
},
];
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const onChangeHandler: TableProps<Exception>['onChange'] = useCallback(
(
paginations: TablePaginationConfig,
@@ -451,7 +458,7 @@ function AllErrors(): JSX.Element {
useEffect(() => {
if (!isUndefined(errorCountResponse.data?.payload)) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === getResourceDeploymentKeys(),
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
)?.tagValue;
logEvent('Exception: List page visited', {

View File

@@ -130,28 +130,6 @@ describe('Footer utils', () => {
};
expect(validateCreateAlertState(currentArgs)).toBeNull();
});
it('when threshold channels are null', () => {
const currentArgs: BuildCreateAlertRulePayloadArgs = {
...args,
basicAlertState: {
...args.basicAlertState,
name: 'test name',
},
thresholdState: {
...args.thresholdState,
thresholds: [
{
...args.thresholdState.thresholds[0],
channels: null as unknown as string[],
},
],
},
};
expect(validateCreateAlertState(currentArgs)).toBe(
'Please select at least one channel for each threshold or enable routing policies',
);
});
});
describe('getNotificationSettingsProps', () => {

View File

@@ -44,8 +44,7 @@ export function validateCreateAlertState(
if (!threshold.label) {
return 'Please enter a label for each threshold';
}
// this runs during render, so a throw here takes down the whole page
if (!notificationSettings.routingPolicies && !threshold.channels?.length) {
if (!notificationSettings.routingPolicies && !threshold.channels.length) {
return 'Please select at least one channel for each threshold or enable routing policies';
}
}

View File

@@ -316,34 +316,6 @@ describe('CreateAlertV2 utils', () => {
});
});
describe('getThresholdStateFromAlertDef null channels', () => {
it('falls back to an empty array so downstream consumers never see null', () => {
const def: PostableAlertRuleV2 = {
...defaultPostableAlertRuleV2,
condition: {
...defaultPostableAlertRuleV2.condition,
thresholds: {
kind: 'basic',
spec: [
{
name: 'critical',
target: 1,
targetUnit: UniversalYAxisUnit.MINUTES,
channels: null as unknown as string[],
matchType: AlertThresholdMatchType.AT_LEAST_ONCE,
op: AlertThresholdOperator.IS_ABOVE,
},
],
},
},
};
expect(
getThresholdStateFromAlertDef(def).thresholds[0].channels,
).toStrictEqual([]);
});
});
describe('normalizeOperator', () => {
it.each([
['1', AlertThresholdOperator.IS_ABOVE],

View File

@@ -258,9 +258,7 @@ export function getThresholdStateFromAlertDef(
recoveryThresholdValue: null,
unit: threshold.targetUnit,
color: getColorForThreshold(threshold.name),
// rules created outside the UI can come back with a null channels
// field; drop the guard once the API enforces the schema
channels: threshold.channels ?? [],
channels: threshold.channels,
})) || [],
selectedQuery: alertDef.condition.selectedQueryName || '',
operator:

View File

@@ -27,6 +27,7 @@ export interface BaseConfigBuilderProps {
panelType: PANEL_TYPES;
minTimeScale?: number;
maxTimeScale?: number;
useExactTimeRange?: boolean;
stepInterval?: number;
isLogScale?: boolean;
yAxisUnit?: string;
@@ -46,6 +47,7 @@ export function buildBaseConfig({
thresholds,
minTimeScale,
maxTimeScale,
useExactTimeRange,
stepInterval,
isLogScale,
yAxisUnit,
@@ -88,6 +90,7 @@ export function buildBaseConfig({
time: true,
min: minTimeScale,
max: maxTimeScale,
useExactTimeRange,
logBase: isLogScale ? 10 : undefined,
distribution: isLogScale
? DistributionType.Logarithmic

View File

@@ -35,6 +35,7 @@ import { openInNewTab } from 'utils/navigation';
import triangleRulerUrl from '@/assets/Icons/triangle-ruler.svg';
import { FeatureKeys } from '../../../constants/features';
import { DOCS_LINKS } from '../constants';
import { columns, TIME_PICKER_OPTIONS } from './constants';
@@ -211,13 +212,19 @@ function ServiceMetrics({
const topLevelOperations = useMemo(() => Object.entries(data || {}), [data]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const queryRangeRequestData = useMemo(
() =>
getQueryRangeRequestData({
topLevelOperations,
globalSelectedInterval,
dotMetricsEnabled,
}),
[globalSelectedInterval, topLevelOperations],
[globalSelectedInterval, topLevelOperations, dotMetricsEnabled],
);
const dataQueries = useGetQueriesRange(

View File

@@ -562,9 +562,13 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
],
having: {
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY}) = 1`,
},
having: [
{
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY})`,
op: '=',
value: 1,
},
],
legend: `{{${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME}}}`,
limit: null,
orderBy: [],
@@ -644,9 +648,13 @@ export const getClusterMetricsQueryPayload = (
type: 'tag',
},
],
having: {
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY}) = 0`,
},
having: [
{
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_CONDITION_READY})`,
op: '=',
value: 0,
},
],
legend: `{{${INFRA_MONITORING_ATTR_KEYS.K8S_NODE_NAME}}}`,
limit: null,
orderBy: [],

View File

@@ -22,7 +22,6 @@ import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants'
import LoadingContainer from 'container/InfraMonitoringK8sV2/LoadingContainer';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import { ChevronDown, ChevronRight } from '@signozhq/icons';
import { saveRecentQueryByExpression } from 'lib/recentQueries/saveRecentQuery';
import { useQueryState } from 'nuqs';
import { DataSource } from 'types/common/queryBuilder';
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
@@ -117,7 +116,6 @@ function EntityEventsContent({
: newUserExpression || '',
);
if (validation.isValid) {
saveRecentQueryByExpression(DataSource.LOGS, newUserExpression);
querySearchOnRun(newUserExpression || '');
void logEvent(InfraMonitoringEvents.FilterApplied, {

View File

@@ -29,7 +29,6 @@ import { getOldLogsOperatorFromNew } from 'hooks/logs/useActiveLog';
import useLogDetailHandlers from 'hooks/logs/useLogDetailHandlers';
import useScrollToLog from 'hooks/logs/useScrollToLog';
import { generateFilterQuery } from 'lib/logs/generateFilterQuery';
import { saveRecentQueryByExpression } from 'lib/recentQueries/saveRecentQuery';
import { ILog } from 'types/api/logs/log';
import { DataSource } from 'types/common/queryBuilder';
import { validateQuery } from 'utils/queryValidationUtils';
@@ -133,7 +132,6 @@ function EntityLogsContent({
);
if (validation.isValid) {
saveRecentQueryByExpression(DataSource.LOGS, newUserExpression);
querySearchOnRun(newUserExpression);
void logEvent(InfraMonitoringEvents.FilterApplied, {

View File

@@ -121,6 +121,12 @@ jest.spyOn(appContextHooks, 'useAppContext').mockReturnValue({
plan_version: 'test-plan-version',
},
},
featureFlags: [
{
name: 'DOT_METRICS_ENABLED',
active: false,
},
],
} as any);
const mockEntity = {

View File

@@ -22,7 +22,6 @@ import { InfraMonitoringEntity } from 'container/InfraMonitoringK8sV2/constants'
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import { PER_PAGE_OPTIONS } from 'container/TracesExplorer/ListView/configs';
import { TracesLoading } from 'container/TracesExplorer/TraceLoading/TraceLoading';
import { saveRecentQueryByExpression } from 'lib/recentQueries/saveRecentQuery';
import { useQueryState } from 'nuqs';
import { DataSource } from 'types/common/queryBuilder';
import { parseAsJsonNoValidate } from 'utils/nuqsParsers';
@@ -99,7 +98,6 @@ function EntityTracesContent({
: newUserExpression || '',
);
if (validation.isValid) {
saveRecentQueryByExpression(DataSource.TRACES, newUserExpression);
querySearchOnRun(newUserExpression || '');
void logEvent(InfraMonitoringEvents.FilterApplied, {

View File

@@ -1208,9 +1208,13 @@ export const getNamespaceMetricsQueryPayload = (
type: 'tag',
},
],
having: {
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED}) > 0`,
},
having: [
{
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED})`,
op: '>',
value: 0,
},
],
legend: 'desired',
limit: null,
orderBy: [],
@@ -1257,9 +1261,13 @@ export const getNamespaceMetricsQueryPayload = (
type: 'tag',
},
],
having: {
expression: `max(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_AVAILABLE}) > 0`,
},
having: [
{
columnName: `MAX(${INFRA_MONITORING_ATTR_KEYS.K8S_REPLICASET_DESIRED})`,
op: '>',
value: 0,
},
],
legend: 'available',
limit: null,
orderBy: [],

View File

@@ -17,6 +17,8 @@ import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import {
getHostQueryPayload,
getNodeQueryPayload,
@@ -51,12 +53,23 @@ function NodeMetrics({
};
}, [timestamp]);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const queryPayloads = useMemo(() => {
if (nodeName) {
return getNodeQueryPayload(clusterName, nodeName, start, end);
return getNodeQueryPayload(
clusterName,
nodeName,
start,
end,
dotMetricsEnabled,
);
}
return getHostQueryPayload(hostName, start, end);
}, [nodeName, hostName, clusterName, start, end]);
return getHostQueryPayload(hostName, start, end, dotMetricsEnabled);
}, [nodeName, hostName, clusterName, start, end, dotMetricsEnabled]);
const widgetInfo = nodeName ? nodeWidgetInfo : hostWidgetInfo;
const queries = useQueries(

View File

@@ -12,11 +12,13 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
import { getUPlotChartOptions } from 'lib/uPlotLib/getUplotChartOptions';
import { getUPlotChartData } from 'lib/uPlotLib/utils/getUplotChartData';
import { useAppContext } from 'providers/App/App';
import { useTimezone } from 'providers/Timezone';
import { SuccessResponse } from 'types/api';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { FeatureKeys } from '../../../constants/features';
import { getPodQueryPayload, podWidgetInfo } from './constants';
function PodMetrics({
@@ -52,9 +54,14 @@ function PodMetrics({
scrollLeft: 0,
});
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const queryPayloads = useMemo(
() => getPodQueryPayload(clusterName, podName, start, end),
[clusterName, end, podName, start],
() => getPodQueryPayload(clusterName, podName, start, end, dotMetricsEnabled),
[clusterName, end, podName, start, dotMetricsEnabled],
);
const queries = useQueries(
queryPayloads.map((payload) => ({

View File

@@ -1,39 +1,56 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import type { Having } from 'types/api/queryBuilder/queryBuilderData';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import type { Having as HavingV5 } from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
const buildSumGreaterThanZeroHaving = (
metricKey: string,
useV5HavingFormat: boolean,
): Having[] | HavingV5 =>
useV5HavingFormat
? { expression: `sum(${metricKey}) > 0` }
: [{ columnName: `SUM(${metricKey})`, op: '>', value: 0 }];
export const getPodQueryPayload = (
clusterName: string,
podName: string,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sClusterNameKey = 'k8s.cluster.name';
const k8sPodNameKey = 'k8s.pod.name';
const containerCpuUtilKey = 'container.cpu.usage';
const containerMemUsageKey = 'container.memory.usage';
const k8sContainerCpuReqKey = 'k8s.container.cpu_request';
const k8sContainerCpuLimitKey = 'k8s.container.cpu_limit';
const k8sContainerMemReqKey = 'k8s.container.memory_request';
const k8sContainerMemLimitKey = 'k8s.container.memory_limit';
const k8sPodFsAvailKey = 'k8s.pod.filesystem.available';
const k8sPodFsCapKey = 'k8s.pod.filesystem.capacity';
const k8sPodNetIoKey = 'k8s.pod.network.io';
const podLegendTemplate = '{{k8s.pod.name}}';
const podLegendUsage = 'usage - {{k8s.pod.name}}';
const podLegendLimit = 'limit - {{k8s.pod.name}}';
const k8sClusterNameKey = dotMetricsEnabled
? 'k8s.cluster.name'
: 'k8s_cluster_name';
const k8sPodNameKey = dotMetricsEnabled ? 'k8s.pod.name' : 'k8s_pod_name';
const containerCpuUtilKey = dotMetricsEnabled
? 'container.cpu.usage'
: 'container_cpu_usage';
const containerMemUsageKey = dotMetricsEnabled
? 'container.memory.usage'
: 'container_memory_usage';
const k8sContainerCpuReqKey = dotMetricsEnabled
? 'k8s.container.cpu_request'
: 'k8s_container_cpu_request';
const k8sContainerCpuLimitKey = dotMetricsEnabled
? 'k8s.container.cpu_limit'
: 'k8s_container_cpu_limit';
const k8sContainerMemReqKey = dotMetricsEnabled
? 'k8s.container.memory_request'
: 'k8s_container_memory_request';
const k8sContainerMemLimitKey = dotMetricsEnabled
? 'k8s.container.memory_limit'
: 'k8s_container_memory_limit';
const k8sPodFsAvailKey = dotMetricsEnabled
? 'k8s.pod.filesystem.available'
: 'k8s_pod_filesystem_available';
const k8sPodFsCapKey = dotMetricsEnabled
? 'k8s.pod.filesystem.capacity'
: 'k8s_pod_filesystem_capacity';
const k8sPodNetIoKey = dotMetricsEnabled
? 'k8s.pod.network.io'
: 'k8s_pod_network_io';
const podLegendTemplate = dotMetricsEnabled
? '{{k8s.pod.name}}'
: '{{k8s_pod_name}}';
const podLegendUsage = dotMetricsEnabled
? 'usage - {{k8s.pod.name}}'
: 'usage - {{k8s_pod_name}}';
const podLegendLimit = dotMetricsEnabled
? 'limit - {{k8s.pod.name}}'
: 'limit - {{k8s_pod_name}}';
return [
{
@@ -1010,17 +1027,36 @@ export const getNodeQueryPayload = (
nodeName: string,
start: number,
end: number,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const k8sClusterNameKey = 'k8s.cluster.name';
const k8sNodeNameKey = 'k8s.node.name';
const k8sNodeCpuTimeKey = 'k8s.node.cpu.time';
const k8sNodeAllocCpuKey = 'k8s.node.allocatable_cpu';
const k8sNodeMemWsKey = 'k8s.node.memory.working_set';
const k8sNodeAllocMemKey = 'k8s.node.allocatable_memory';
const k8sNodeNetIoKey = 'k8s.node.network.io';
const k8sNodeFsAvailKey = 'k8s.node.filesystem.available';
const k8sNodeFsCapKey = 'k8s.node.filesystem.capacity';
const podLegend = '{{k8s.node.name}}';
const k8sClusterNameKey = dotMetricsEnabled
? 'k8s.cluster.name'
: 'k8s_cluster_name';
const k8sNodeNameKey = dotMetricsEnabled ? 'k8s.node.name' : 'k8s_node_name';
const k8sNodeCpuTimeKey = dotMetricsEnabled
? 'k8s.node.cpu.time'
: 'k8s_node_cpu_time';
const k8sNodeAllocCpuKey = dotMetricsEnabled
? 'k8s.node.allocatable_cpu'
: 'k8s_node_allocatable_cpu';
const k8sNodeMemWsKey = dotMetricsEnabled
? 'k8s.node.memory.working_set'
: 'k8s_node_memory_working_set';
const k8sNodeAllocMemKey = dotMetricsEnabled
? 'k8s.node.allocatable_memory'
: 'k8s_node_allocatable_memory';
const k8sNodeNetIoKey = dotMetricsEnabled
? 'k8s.node.network.io'
: 'k8s_node_network_io';
const k8sNodeFsAvailKey = dotMetricsEnabled
? 'k8s.node.filesystem.available'
: 'k8s_node_filesystem_available';
const k8sNodeFsCapKey = dotMetricsEnabled
? 'k8s.node.filesystem.capacity'
: 'k8s_node_filesystem_capacity';
const podLegend = dotMetricsEnabled
? '{{k8s.node.name}}'
: '{{k8s_node_name}}';
return [
{
@@ -1550,24 +1586,48 @@ export const getHostQueryPayload = (
hostName: string,
start: number,
end: number,
useV5HavingFormat = false,
dotMetricsEnabled: boolean,
): GetQueryResultsProps[] => {
const hostNameKey = 'host.name';
const cpuTimeKey = 'system.cpu.time';
const memUsageKey = 'system.memory.usage';
const load1mKey = 'system.cpu.load_average.1m';
const load5mKey = 'system.cpu.load_average.5m';
const load15mKey = 'system.cpu.load_average.15m';
const netIoKey = 'system.network.io';
const netPktsKey = 'system.network.packets';
const netErrKey = 'system.network.errors';
const netDropKey = 'system.network.dropped';
const netConnKey = 'system.network.connections';
const diskIoKey = 'system.disk.io';
const diskOpTimeKey = 'system.disk.operation_time';
const diskOpsKey = 'system.disk.operations';
const diskPendingKey = 'system.disk.pending_operations';
const fsUsageKey = 'system.filesystem.usage';
const hostNameKey = dotMetricsEnabled ? 'host.name' : 'host_name';
const cpuTimeKey = dotMetricsEnabled ? 'system.cpu.time' : 'system_cpu_time';
const memUsageKey = dotMetricsEnabled
? 'system.memory.usage'
: 'system_memory_usage';
const load1mKey = dotMetricsEnabled
? 'system.cpu.load_average.1m'
: 'system_cpu_load_average_1m';
const load5mKey = dotMetricsEnabled
? 'system.cpu.load_average.5m'
: 'system_cpu_load_average_5m';
const load15mKey = dotMetricsEnabled
? 'system.cpu.load_average.15m'
: 'system_cpu_load_average_15m';
const netIoKey = dotMetricsEnabled ? 'system.network.io' : 'system_network_io';
const netPktsKey = dotMetricsEnabled
? 'system.network.packets'
: 'system_network_packets';
const netErrKey = dotMetricsEnabled
? 'system.network.errors'
: 'system_network_errors';
const netDropKey = dotMetricsEnabled
? 'system.network.dropped'
: 'system_network_dropped';
const netConnKey = dotMetricsEnabled
? 'system.network.connections'
: 'system_network_connections';
const diskIoKey = dotMetricsEnabled ? 'system.disk.io' : 'system_disk_io';
const diskOpTimeKey = dotMetricsEnabled
? 'system.disk.operation_time'
: 'system_disk_operation_time';
const diskOpsKey = dotMetricsEnabled
? 'system.disk.operations'
: 'system_disk_operations';
const diskPendingKey = dotMetricsEnabled
? 'system.disk.pending_operations'
: 'system_disk_pending_operations';
const fsUsageKey = dotMetricsEnabled
? 'system.filesystem.usage'
: 'system_filesystem_usage';
return [
{
@@ -1813,7 +1873,13 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
having: [
{
columnName: `SUM(${fsUsageKey})`,
op: '>',
value: 0,
},
],
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
@@ -1862,7 +1928,13 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(fsUsageKey, useV5HavingFormat),
having: [
{
columnName: `SUM(${fsUsageKey})`,
op: '>',
value: 0,
},
],
legend: '{{mountpoint}}',
limit: null,
orderBy: [],
@@ -2088,7 +2160,13 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(netIoKey, useV5HavingFormat),
having: [
{
columnName: `SUM(${netIoKey})`,
op: '>',
value: 0,
},
],
legend: '{{device}}::{{direction}}',
limit: 30,
orderBy: [],
@@ -2544,7 +2622,13 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(diskOpsKey, useV5HavingFormat),
having: [
{
columnName: `SUM(${diskOpsKey})`,
op: '>',
value: 0,
},
],
legend: '{{device}}::{{direction}}',
limit: null,
orderBy: [],
@@ -2613,7 +2697,13 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(diskPendingKey, useV5HavingFormat),
having: [
{
columnName: `SUM(${diskPendingKey})`,
op: '>',
value: 0,
},
],
legend: '{{device}}',
limit: null,
orderBy: [],
@@ -2689,7 +2779,13 @@ export const getHostQueryPayload = (
type: 'tag',
},
],
having: buildSumGreaterThanZeroHaving(diskOpTimeKey, useV5HavingFormat),
having: [
{
columnName: `SUM(${diskOpTimeKey})`,
op: '>',
value: 0,
},
],
legend: '{{device}}::{{direction}}',
limit: null,
orderBy: [],

View File

@@ -21,6 +21,7 @@ export const databaseCallsRPS = ({
servicename,
legend,
tagFilterItems,
dotMetricsEnabled,
}: DatabaseCallsRPSProps): QueryBuilderData => {
const autocompleteData: BaseAutocompleteData[] = [
{
@@ -32,7 +33,7 @@ export const databaseCallsRPS = ({
const groupBy: BaseAutocompleteData[] = [
{
dataType: DataTypes.String,
key: WidgetKeys.DbSystem,
key: dotMetricsEnabled ? WidgetKeys.Db_system : WidgetKeys.Db_system_norm,
type: 'tag',
},
];
@@ -41,7 +42,9 @@ export const databaseCallsRPS = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},
@@ -72,6 +75,7 @@ export const databaseCallsRPS = ({
export const databaseCallsAvgDuration = ({
servicename,
tagFilterItems,
dotMetricsEnabled,
}: DatabaseCallProps): QueryBuilderData => {
const autocompleteDataA: BaseAutocompleteData = {
key: WidgetKeys.SignozDbLatencySum,
@@ -88,7 +92,9 @@ export const databaseCallsAvgDuration = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},

View File

@@ -32,6 +32,7 @@ export const externalCallErrorPercent = ({
servicename,
legend,
tagFilterItems,
dotMetricsEnabled,
}: ExternalCallDurationByAddressProps): QueryBuilderData => {
const autocompleteDataA: BaseAutocompleteData = {
key: WidgetKeys.SignozExternalCallLatencyCount,
@@ -48,7 +49,9 @@ export const externalCallErrorPercent = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},
@@ -58,7 +61,7 @@ export const externalCallErrorPercent = ({
{
id: '',
key: {
key: WidgetKeys.StatusCode,
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
dataType: DataTypes.Int64,
type: MetricsType.Tag,
},
@@ -71,7 +74,9 @@ export const externalCallErrorPercent = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},
@@ -115,6 +120,7 @@ export const externalCallErrorPercent = ({
export const externalCallDuration = ({
servicename,
tagFilterItems,
dotMetricsEnabled,
}: ExternalCallProps): QueryBuilderData => {
const autocompleteDataA: BaseAutocompleteData = {
dataType: DataTypes.Float64,
@@ -135,7 +141,9 @@ export const externalCallDuration = ({
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -175,6 +183,7 @@ export const externalCallRpsByAddress = ({
servicename,
legend,
tagFilterItems,
dotMetricsEnabled,
}: ExternalCallDurationByAddressProps): QueryBuilderData => {
const autocompleteData: BaseAutocompleteData[] = [
{
@@ -189,7 +198,9 @@ export const externalCallRpsByAddress = ({
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -220,6 +231,7 @@ export const externalCallDurationByAddress = ({
servicename,
legend,
tagFilterItems,
dotMetricsEnabled,
}: ExternalCallDurationByAddressProps): QueryBuilderData => {
const autocompleteDataA: BaseAutocompleteData = {
dataType: DataTypes.Float64,
@@ -239,7 +251,9 @@ export const externalCallDurationByAddress = ({
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,

View File

@@ -37,10 +37,15 @@ export const latency = ({
tagFilterItems,
isSpanMetricEnable = false,
topLevelOperationsRoute,
dotMetricsEnabled,
}: LatencyProps): QueryBuilderData => {
const signozLatencyBucketMetrics = WidgetKeys.SignozLatencyBucket;
const signozLatencyBucketMetrics = dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm;
const signozMetricsServiceName = WidgetKeys.OTelServiceName;
const signozMetricsServiceName = dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm;
const newAutoCompleteData: BaseAutocompleteData = {
key: isSpanMetricEnable
? signozLatencyBucketMetrics
@@ -282,21 +287,28 @@ export const apDexMetricsQueryBuilderQueries = ({
threashold,
delta,
metricsBuckets,
dotMetricsEnabled,
}: ApDexMetricsQueryBuilderQueriesProps): QueryBuilderData => {
const autoCompleteDataA: BaseAutocompleteData = {
key: WidgetKeys.SignozLatencyCount,
key: dotMetricsEnabled
? WidgetKeys.SignozLatencyCount
: WidgetKeys.SignozLatencyCountNorm,
dataType: DataTypes.Float64,
type: '',
};
const autoCompleteDataB: BaseAutocompleteData = {
key: WidgetKeys.SignozLatencyBucket,
key: dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm,
dataType: DataTypes.Float64,
type: '',
};
const autoCompleteDataC: BaseAutocompleteData = {
key: WidgetKeys.SignozLatencyBucket,
key: dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm,
dataType: DataTypes.Float64,
type: '',
};
@@ -305,7 +317,9 @@ export const apDexMetricsQueryBuilderQueries = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Tag,
},
@@ -329,7 +343,7 @@ export const apDexMetricsQueryBuilderQueries = ({
{
id: '',
key: {
key: WidgetKeys.StatusCode,
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
dataType: DataTypes.String,
type: MetricsType.Tag,
},
@@ -349,7 +363,9 @@ export const apDexMetricsQueryBuilderQueries = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Tag,
},
@@ -383,7 +399,7 @@ export const apDexMetricsQueryBuilderQueries = ({
{
id: '',
key: {
key: WidgetKeys.StatusCode,
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
dataType: DataTypes.String,
type: MetricsType.Tag,
},
@@ -393,7 +409,9 @@ export const apDexMetricsQueryBuilderQueries = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Tag,
},
@@ -456,10 +474,13 @@ export const operationPerSec = ({
servicename,
tagFilterItems,
topLevelOperations,
dotMetricsEnabled,
}: OperationPerSecProps): QueryBuilderData => {
const autocompleteData: BaseAutocompleteData[] = [
{
key: WidgetKeys.SignozLatencyCount,
key: dotMetricsEnabled
? WidgetKeys.SignozLatencyCount
: WidgetKeys.SignozLatencyCountNorm,
dataType: DataTypes.Float64,
type: '',
},
@@ -470,7 +491,9 @@ export const operationPerSec = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},
@@ -511,6 +534,7 @@ export const errorPercentage = ({
servicename,
tagFilterItems,
topLevelOperations,
dotMetricsEnabled,
}: OperationPerSecProps): QueryBuilderData => {
const autocompleteDataA: BaseAutocompleteData = {
key: WidgetKeys.SignozCallsTotal,
@@ -529,7 +553,9 @@ export const errorPercentage = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},
@@ -549,7 +575,7 @@ export const errorPercentage = ({
{
id: '',
key: {
key: WidgetKeys.StatusCode,
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
dataType: DataTypes.Int64,
type: MetricsType.Tag,
},
@@ -563,7 +589,9 @@ export const errorPercentage = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},

View File

@@ -21,9 +21,12 @@ import { getQueryBuilderQuerieswithFormula } from './MetricsPageQueriesFactory';
export const topOperationQueries = ({
servicename,
dotMetricsEnabled,
}: TopOperationQueryFactoryProps): QueryBuilderData => {
const latencyAutoCompleteData: BaseAutocompleteData = {
key: WidgetKeys.SignozLatencyBucket,
key: dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm,
dataType: DataTypes.Float64,
type: '',
};
@@ -35,7 +38,9 @@ export const topOperationQueries = ({
};
const numOfCallAutoCompleteData: BaseAutocompleteData = {
key: WidgetKeys.SignozLatencyCount,
key: dotMetricsEnabled
? WidgetKeys.SignozLatencyCount
: WidgetKeys.SignozLatencyCountNorm,
dataType: DataTypes.Float64,
type: '',
};
@@ -44,7 +49,9 @@ export const topOperationQueries = ({
{
id: '',
key: {
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
dataType: DataTypes.String,
type: MetricsType.Resource,
},
@@ -58,7 +65,9 @@ export const topOperationQueries = ({
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -68,7 +77,7 @@ export const topOperationQueries = ({
id: '',
key: {
dataType: DataTypes.Int64,
key: WidgetKeys.StatusCode,
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
type: MetricsType.Tag,
},
op: OPERATORS.IN,

View File

@@ -28,6 +28,8 @@ import { TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import {
GraphTitle,
MENU_ITEMS,
@@ -87,7 +89,12 @@ function DBCall(): JSX.Element {
[queries],
);
const legend = '{{db.system}}';
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const legend = dotMetricsEnabled ? '{{db.system}}' : '{{db_system}}';
const databaseCallsRPSWidget = useMemo(
() =>
@@ -99,6 +106,7 @@ function DBCall(): JSX.Element {
servicename,
legend,
tagFilterItems,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -109,7 +117,7 @@ function DBCall(): JSX.Element {
id: SERVICE_CHART_ID.dbCallsRPS,
fillSpans: false,
}),
[servicename, tagFilterItems, legend],
[servicename, tagFilterItems, dotMetricsEnabled, legend],
);
const databaseCallsAverageDurationWidget = useMemo(
() =>
@@ -120,6 +128,7 @@ function DBCall(): JSX.Element {
builder: databaseCallsAvgDuration({
servicename,
tagFilterItems,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -130,7 +139,7 @@ function DBCall(): JSX.Element {
id: GraphTitle.DATABASE_CALLS_AVG_DURATION,
fillSpans: true,
}),
[servicename, tagFilterItems],
[servicename, tagFilterItems, dotMetricsEnabled],
);
const stepInterval = useMemo(
@@ -148,7 +157,7 @@ function DBCall(): JSX.Element {
useEffect(() => {
if (!logEventCalledRef.current) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === getResourceDeploymentKeys(),
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
)?.tagValue;
logEvent('APM: Service detail page visited', {

View File

@@ -30,6 +30,8 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import {
GraphTitle,
legend,
@@ -82,6 +84,10 @@ function External(): JSX.Element {
handleNonInQueryRange(resourceAttributesToTagFilterItems(queries)) || [],
[queries],
);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const externalCallErrorWidget = useMemo(
() =>
@@ -93,6 +99,7 @@ function External(): JSX.Element {
servicename,
legend: legend.address,
tagFilterItems,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -102,7 +109,7 @@ function External(): JSX.Element {
yAxisUnit: '%',
id: GraphTitle.EXTERNAL_CALL_ERROR_PERCENTAGE,
}),
[servicename, tagFilterItems],
[servicename, tagFilterItems, dotMetricsEnabled],
);
const selectedTraceTags = useMemo(
@@ -119,6 +126,7 @@ function External(): JSX.Element {
builder: externalCallDuration({
servicename,
tagFilterItems,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -129,7 +137,7 @@ function External(): JSX.Element {
id: GraphTitle.EXTERNAL_CALL_DURATION,
fillSpans: true,
}),
[servicename, tagFilterItems],
[servicename, tagFilterItems, dotMetricsEnabled],
);
const errorApmToTraceQuery = useGetAPMToTracesQueries({
@@ -163,7 +171,7 @@ function External(): JSX.Element {
useEffect(() => {
if (!logEventCalledRef.current) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === getResourceDeploymentKeys(),
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
)?.tagValue;
logEvent('APM: Service detail page visited', {
@@ -186,6 +194,7 @@ function External(): JSX.Element {
servicename,
legend: legend.address,
tagFilterItems,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -196,7 +205,7 @@ function External(): JSX.Element {
id: GraphTitle.EXTERNAL_CALL_RPS_BY_ADDRESS,
fillSpans: true,
}),
[servicename, tagFilterItems],
[servicename, tagFilterItems, dotMetricsEnabled],
);
const externalCallDurationAddressWidget = useMemo(
@@ -209,6 +218,7 @@ function External(): JSX.Element {
servicename,
legend: legend.address,
tagFilterItems,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -219,7 +229,7 @@ function External(): JSX.Element {
id: GraphTitle.EXTERNAL_CALL_DURATION_BY_ADDRESS,
fillSpans: true,
}),
[servicename, tagFilterItems],
[servicename, tagFilterItems, dotMetricsEnabled],
);
const apmToTraceQuery = useGetAPMToTracesQueries({

View File

@@ -93,12 +93,15 @@ function Application(): JSX.Element {
// eslint-disable-next-line react-hooks/exhaustive-deps
[handleSetTimeStamp],
);
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const logEventCalledRef = useRef(false);
useEffect(() => {
if (!logEventCalledRef.current) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === getResourceDeploymentKeys(),
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
)?.tagValue;
logEvent('APM: Service detail page visited', {
@@ -156,6 +159,7 @@ function Application(): JSX.Element {
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -165,7 +169,7 @@ function Application(): JSX.Element {
yAxisUnit: 'ops',
id: SERVICE_CHART_ID.rps,
}),
[servicename, tagFilterItems, topLevelOperationsRoute],
[servicename, tagFilterItems, topLevelOperationsRoute, dotMetricsEnabled],
);
const errorPercentageWidget = useMemo(
@@ -178,6 +182,7 @@ function Application(): JSX.Element {
servicename,
tagFilterItems,
topLevelOperations: topLevelOperationsRoute,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -188,7 +193,7 @@ function Application(): JSX.Element {
id: SERVICE_CHART_ID.errorPercentage,
fillSpans: true,
}),
[servicename, tagFilterItems, topLevelOperationsRoute],
[servicename, tagFilterItems, topLevelOperationsRoute, dotMetricsEnabled],
);
const stepInterval = useMemo(

View File

@@ -22,6 +22,8 @@ import { apDexMetricsQueryBuilderQueries } from 'container/MetricsApplication/Me
import { EQueryType } from 'types/common/dashboard';
import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../../../constants/features';
import { useAppContext } from '../../../../../providers/App/App';
import { IServiceName } from '../../types';
import { ApDexMetricsProps } from './types';
@@ -36,6 +38,10 @@ function ApDexMetrics({
}: ApDexMetricsProps): JSX.Element {
const { servicename: encodedServiceName } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const apDexMetricsWidget = useMemo(
() =>
getWidgetQueryBuilder({
@@ -49,6 +55,7 @@ function ApDexMetrics({
threashold: thresholdValue || 0,
delta: delta || false,
metricsBuckets: metricsBuckets || [],
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -74,6 +81,7 @@ function ApDexMetrics({
tagFilterItems,
thresholdValue,
topLevelOperationsRoute,
dotMetricsEnabled,
],
);

View File

@@ -3,6 +3,8 @@ import Spinner from 'components/Spinner';
import { useGetMetricMeta } from 'hooks/apDex/useGetMetricMeta';
import useErrorNotification from 'hooks/useErrorNotification';
import { FeatureKeys } from '../../../../../constants/features';
import { useAppContext } from '../../../../../providers/App/App';
import { WidgetKeys } from '../../../constant';
import { IServiceName } from '../../types';
import ApDexMetrics from './ApDexMetrics';
@@ -18,8 +20,17 @@ function ApDexMetricsApplication({
const { servicename: encodedServiceName } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const signozLatencyBucketMetrics = dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm;
const { data, isLoading, error } = useGetMetricMeta(
WidgetKeys.SignozLatencyBucket,
signozLatencyBucketMetrics,
servicename,
);
useErrorNotification(error);

View File

@@ -56,6 +56,10 @@ function ServiceOverview({
[isSpanMetricEnable, queries],
);
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const latencyWidget = useMemo(
() =>
getWidgetQueryBuilder({
@@ -67,6 +71,7 @@ function ServiceOverview({
tagFilterItems,
isSpanMetricEnable,
topLevelOperationsRoute,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
@@ -76,7 +81,13 @@ function ServiceOverview({
yAxisUnit: 'ns',
id: SERVICE_CHART_ID.latency,
}),
[isSpanMetricEnable, servicename, tagFilterItems, topLevelOperationsRoute],
[
isSpanMetricEnable,
servicename,
tagFilterItems,
topLevelOperationsRoute,
dotMetricsEnabled,
],
);
const isQueryEnabled =

View File

@@ -19,6 +19,8 @@ import { EQueryType } from 'types/common/dashboard';
import { GlobalReducer } from 'types/reducer/globalTime';
import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../../constants/features';
import { useAppContext } from '../../../../providers/App/App';
import { IServiceName } from '../types';
import { title } from './config';
import ColumnWithLink from './TableRenderer/ColumnWithLink';
@@ -42,6 +44,11 @@ function TopOperationMetrics(): JSX.Element {
convertRawQueriesToTraceSelectedTags(queries) || [],
);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const keyOperationWidget = useMemo(
() =>
getWidgetQueryBuilder({
@@ -50,13 +57,14 @@ function TopOperationMetrics(): JSX.Element {
promql: [],
builder: topOperationQueries({
servicename,
dotMetricsEnabled,
}),
clickhouse_sql: [],
id: uuid(),
},
panelTypes: PANEL_TYPES.TABLE,
}),
[servicename],
[servicename, dotMetricsEnabled],
);
const updatedQuery = updateStepInterval(keyOperationWidget.query);

View File

@@ -10,6 +10,7 @@ export interface IServiceName {
export interface TopOperationQueryFactoryProps {
servicename: IServiceName['servicename'];
dotMetricsEnabled: boolean;
}
export interface ExternalCallDurationByAddressProps extends ExternalCallProps {
@@ -19,6 +20,7 @@ export interface ExternalCallDurationByAddressProps extends ExternalCallProps {
export interface ExternalCallProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
dotMetricsEnabled: boolean;
}
export interface BuilderQueriesProps {
@@ -50,6 +52,7 @@ export interface OperationPerSecProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
topLevelOperations: string[];
dotMetricsEnabled: boolean;
}
export interface LatencyProps {
@@ -57,6 +60,7 @@ export interface LatencyProps {
tagFilterItems: TagFilterItem[];
isSpanMetricEnable?: boolean;
topLevelOperationsRoute: string[];
dotMetricsEnabled: boolean;
}
export interface ApDexProps {
@@ -74,4 +78,5 @@ export interface TableRendererProps {
export interface ApDexMetricsQueryBuilderQueriesProps extends ApDexProps {
delta: boolean;
metricsBuckets: number[];
dotMetricsEnabled: boolean;
}

View File

@@ -85,11 +85,14 @@ export enum WidgetKeys {
HasError = 'hasError',
Address = 'address',
DurationNano = 'durationNano',
StatusCodeNorm = 'status_code',
StatusCode = 'status.code',
Operation = 'operation',
OperationName = 'operationName',
OTelServiceName = 'service.name',
Service_name_norm = 'service_name',
Service_name = 'service.name',
ServiceName = 'serviceName',
SignozLatencyCountNorm = 'signoz_latency_count',
SignozLatencyCount = 'signoz_latency.count',
SignozDBLatencyCount = 'signoz_db_latency_count',
DatabaseCallCount = 'signoz_database_call_count',
@@ -98,8 +101,10 @@ export enum WidgetKeys {
SignozCallsTotal = 'signoz_calls_total',
SignozExternalCallLatencyCount = 'signoz_external_call_latency_count',
SignozExternalCallLatencySum = 'signoz_external_call_latency_sum',
SignozLatencyBucket = 'signoz_latency.bucket',
DbSystem = 'db.system',
Signoz_latency_bucket_norm = 'signoz_latency_bucket',
Signoz_latency_bucket = 'signoz_latency.bucket',
Db_system = 'db.system',
Db_system_norm = 'db_system',
}
export const topOperationMetricsDownloadOptions: DownloadOptions = {

View File

@@ -32,4 +32,5 @@ export interface DatabaseCallsRPSProps extends DatabaseCallProps {
export interface DatabaseCallProps {
servicename: IServiceName['servicename'];
tagFilterItems: TagFilterItem[];
dotMetricsEnabled: boolean;
}

View File

@@ -2,7 +2,6 @@ import { useCallback } from 'react';
import QuerySearch from 'components/QueryBuilderV2/QueryV2/QuerySearch/QuerySearch';
import RunQueryBtn from 'container/QueryBuilder/components/RunQueryBtn/RunQueryBtn';
import DateTimeSelectionV2 from 'container/TopNav/DateTimeSelectionV2';
import { saveRecentQueryByExpression } from 'lib/recentQueries/saveRecentQuery';
import { DataSource } from 'types/common/queryBuilder';
import { MetricsSearchProps } from './types';
@@ -24,14 +23,12 @@ function MetricsSearch({
);
const handleStageAndRunQuery = useCallback(() => {
saveRecentQueryByExpression(DataSource.METRICS, currentQueryFilterExpression);
onChange(currentQueryFilterExpression);
onRunQuery?.();
}, [currentQueryFilterExpression, onChange, onRunQuery]);
const handleRunQuery = useCallback(
(expression: string): void => {
saveRecentQueryByExpression(DataSource.METRICS, expression);
setCurrentQueryFilterExpression(expression);
onChange(expression);
},

View File

@@ -387,42 +387,4 @@ describe('useOptionsMenu', () => {
expect(remaining).toHaveLength(seedColumns.length);
});
});
describe('fieldsSelector.value drops legacy columns without a name', () => {
it('excludes entries missing name while keeping valid columns', () => {
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
data: { data: { data: { keys: {} } } },
isFetching: false,
});
(usePreferenceContext as jest.Mock).mockReturnValue({
traces: {
preferences: {
columns: [
{ name: 'body', fieldContext: 'log' },
{ key: 'legacy-key-no-name', fieldContext: 'log' },
{ name: 'timestamp', fieldContext: 'log' },
],
formatting: { format: 'table', maxLines: 1, fontSize: 'small' },
},
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
logs: {
preferences: { columns: [], formatting: {} },
updateColumns: mockUpdateColumns,
updateFormatting: mockUpdateFormatting,
},
});
const { result } = renderHook(() =>
useOptionsMenu({
dataSource: DataSource.TRACES,
aggregateOperator: 'count',
}),
);
const fields = result.current.config.fieldsSelector?.value ?? [];
expect(fields.map((f) => f.name)).toStrictEqual(['body', 'timestamp']);
});
});
});

View File

@@ -399,7 +399,7 @@ const useOptionsMenu = ({
onReorder: reorderSelectColumns,
},
fieldsSelector: {
value: preferences?.columns?.filter((item) => has(item, 'name')) ?? [],
value: preferences?.columns ?? [],
onFieldsChange: updateColumns,
},
format: {

View File

@@ -53,6 +53,8 @@ import { getUserOperatingSystem, UserOperatingSystem } from 'utils/getUserOS';
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../../../constants/features';
import { useAppContext } from '../../../../providers/App/App';
import { selectStyle } from './config';
import { PLACEHOLDER } from './constant';
import ExampleQueriesRendererForLogs from './ExampleQueriesRendererForLogs';
@@ -102,6 +104,11 @@ function QueryBuilderSearch({
const [isEditingTag, setIsEditingTag] = useState(false);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const {
updateTag,
handleClearTag,
@@ -121,6 +128,7 @@ function QueryBuilderSearch({
exampleQueries,
} = useAutoComplete(
query,
dotMetricsEnabled,
whereClauseConfig,
isLogsExplorerPage,
isInfraMonitoring,
@@ -138,6 +146,7 @@ function QueryBuilderSearch({
const { sourceKeys, handleRemoveSourceKey } = useFetchKeysAndValues(
searchValue,
query,
dotMetricsEnabled,
searchKey,
isLogsExplorerPage,
isInfraMonitoring,

View File

@@ -33,7 +33,7 @@ jest.mock('hooks/useNotifications', () => ({
}),
}));
const RESET_PASSWORD_ENDPOINT = '*/api/v2/factor_password/reset';
const RESET_PASSWORD_ENDPOINT = '*/resetPassword';
const mockHistoryPush = history.push as jest.MockedFunction<
typeof history.push

View File

@@ -1,12 +1,11 @@
import { useMemo, useState } from 'react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation } from 'react-use';
import { Button } from '@signozhq/ui/button';
import { Callout } from '@signozhq/ui/callout';
import { Form, Input as AntdInput } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
import { useResetPassword } from 'api/generated/services/users';
import resetPasswordApi from 'api/v1/factor_password/resetPassword';
import AuthError from 'components/AuthError/AuthError';
import AuthPageContainer from 'components/AuthPageContainer';
import ROUTES from 'constants/routes';
@@ -15,6 +14,7 @@ import { useNotifications } from 'hooks/useNotifications';
import history from 'lib/history';
import { ArrowRight, CircleAlert, KeyRound } from '@signozhq/icons';
import { Label } from 'pages/SignUp/styles';
import APIError from 'types/api/error';
import { FormContainer } from './styles';
@@ -26,41 +26,40 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
const [confirmPasswordError, setConfirmPasswordError] =
useState<boolean>(false);
const [errorMessage, setErrorMessage] = useState<APIError | null>();
const [isValidPassword, setIsValidPassword] = useState(false);
const [loading, setLoading] = useState(false);
const { t } = useTranslation(['common']);
const { search } = useLocation();
const params = new URLSearchParams(search);
const token = params.get('token');
const { notifications } = useNotifications();
const {
mutate: resetPassword,
isLoading,
error: mutationError,
} = useResetPassword();
const errorMessage = useMemo(
() => convertToApiError(mutationError),
[mutationError],
);
const [form] = Form.useForm<FormValues>();
const handleFormSubmit = (): void => {
const { password } = form.getFieldsValue();
const handleFormSubmit: () => Promise<void> = async () => {
try {
setLoading(true);
setErrorMessage(null);
const { password } = form.getFieldsValue();
resetPassword(
{ data: { password, token: token || '' } },
{
onSuccess: (): void => {
notifications.success({
message: t('success', {
ns: 'common',
}),
});
history.push(ROUTES.LOGIN);
},
},
);
await resetPasswordApi({
password,
token: token || '',
});
notifications.success({
message: t('success', {
ns: 'common',
}),
});
history.push(ROUTES.LOGIN);
setLoading(false);
} catch (error) {
setLoading(false);
setErrorMessage(error as APIError);
}
};
const validatePassword = (): boolean => {
@@ -223,7 +222,7 @@ function ResetPassword({ version }: ResetPasswordProps): JSX.Element {
color="primary"
type="submit"
data-attr="reset-password"
disabled={!isValidPassword || isLoading}
disabled={!isValidPassword || loading}
className="reset-password-submit-button"
suffix={<ArrowRight size={16} />}
>

View File

@@ -14,6 +14,8 @@ import { SelectOption } from 'types/common/select';
import { popupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
import { FeatureKeys } from '../../constants/features';
import { useAppContext } from '../../providers/App/App';
import QueryChip from './components/QueryChip';
import { QueryChipItem, SearchContainer } from './styles';
@@ -40,7 +42,12 @@ function ResourceAttributesFilter({
SelectOption<string, string>[]
>([]);
const resourceDeploymentKey = getResourceDeploymentKeys();
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const resourceDeploymentKey = getResourceDeploymentKeys(dotMetricsEnabled);
const [selectedEnvironments, setSelectedEnvironments] = useState<string[]>([]);
@@ -66,20 +73,21 @@ function ResourceAttributesFilter({
}, [queries, resourceDeploymentKey]);
useEffect(() => {
getEnvironmentTagKeys().then((tagKeys) => {
getEnvironmentTagKeys(dotMetricsEnabled).then((tagKeys) => {
if (tagKeys && Array.isArray(tagKeys) && tagKeys.length > 0) {
getEnvironmentTagValues().then((tagValues) => {
getEnvironmentTagValues(dotMetricsEnabled).then((tagValues) => {
setEnvironments(tagValues);
});
}
});
}, []);
}, [dotMetricsEnabled]);
return (
<div className="resourceAttributesFilter-container">
<div className="environment-selector">
<Select
getPopupContainer={popupContainer}
key={selectedEnvironments.join('')}
showSearch
mode="multiple"
value={selectedEnvironments}

View File

@@ -1,175 +0,0 @@
import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Router } from 'react-router-dom';
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import ROUTES from 'constants/routes';
import { createMemoryHistory, MemoryHistory } from 'history';
import { ResourceProvider } from 'hooks/useResourceAttribute';
import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
import { encode } from 'js-base64';
import ResourceAttributesFilter from '../ResourceAttributesFilter';
jest.mock('lib/history', () => ({
__esModule: true,
default: {
push: jest.fn(),
location: { search: '', pathname: '/' },
},
}));
jest.mock('api/metrics/getResourceAttributes', () => ({
getResourceAttributesTagKeys: jest.fn(),
getResourceAttributesTagValues: jest.fn(),
}));
// eslint-disable-next-line import/first, import/order
import {
getResourceAttributesTagKeys,
getResourceAttributesTagValues,
// eslint-disable-next-line import/newline-after-import
} from 'api/metrics/getResourceAttributes';
// eslint-disable-next-line import/first, import/order
import history from 'lib/history';
const mockTagKeys = getResourceAttributesTagKeys as jest.MockedFunction<
typeof getResourceAttributesTagKeys
>;
const mockTagValues = getResourceAttributesTagValues as jest.MockedFunction<
typeof getResourceAttributesTagValues
>;
function tagKeysPayload(keys: string[]): never {
return {
statusCode: 200,
error: null,
message: 'ok',
payload: {
data: {
attributeKeys: keys.map((key) => ({
key,
dataType: 'string',
type: 'resource',
isColumn: false,
})),
},
},
} as unknown as never;
}
function tagValuesPayload(values: string[]): never {
return {
statusCode: 200,
error: null,
message: 'ok',
payload: { data: { stringAttributeValues: values } },
} as unknown as never;
}
function seedUrl(queries: IResourceAttribute[], pathname: string): void {
const location = history.location as { search: string; pathname: string };
location.search = queries.length
? `?resourceAttribute=${encode(JSON.stringify(queries))}`
: '';
location.pathname = pathname;
}
function renderFilter(pathname: string): MemoryHistory {
const routerHistory = createMemoryHistory({
initialEntries: [`${pathname}${history.location.search}`],
});
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
function Wrapper({ children }: { children: ReactNode }): JSX.Element {
return (
<QueryClientProvider client={queryClient}>
<Router history={routerHistory}>
<ResourceProvider>{children}</ResourceProvider>
</Router>
</QueryClientProvider>
);
}
render(
<Wrapper>
<ResourceAttributesFilter />
</Wrapper>,
);
return routerHistory;
}
describe('ResourceAttributesFilter', () => {
beforeEach(() => {
mockTagKeys.mockReset();
mockTagValues.mockReset();
mockTagKeys.mockResolvedValue(
tagKeysPayload(['resource_deployment.environment']),
);
mockTagValues.mockResolvedValue(tagValuesPayload(['production', 'staging']));
seedUrl([], '/');
});
it('shows every applied filter on the service map, including ones it cannot apply', async () => {
seedUrl(
[
{
id: 'svc',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
},
{
id: 'env',
tagKey: 'resource_deployment.environment',
operator: 'IN',
tagValue: ['production'],
},
],
ROUTES.SERVICE_MAP,
);
renderFilter(ROUTES.SERVICE_MAP);
await waitFor(() =>
expect(screen.getByText(/service\.name/)).toBeInTheDocument(),
);
await waitFor(() =>
expect(
screen
.getByTestId('resource-environment-filter')
.querySelector('.ant-select-selection-item'),
).toHaveTextContent('production'),
);
});
it('keeps the environment dropdown open so more than one environment can be picked', async () => {
const user = userEvent.setup();
renderFilter('/services');
const environmentFilter = screen.getByTestId('resource-environment-filter');
await user.click(
environmentFilter.querySelector('input') as HTMLInputElement,
);
await user.click(await screen.findByTitle('production'));
await waitFor(() =>
expect(
screen.getByTitle('staging').closest('.ant-select-dropdown'),
).not.toHaveClass('ant-select-dropdown-hidden'),
);
await user.click(screen.getByTitle('staging'));
await waitFor(() => {
const selected = Array.from(
environmentFilter.querySelectorAll('.ant-select-selection-item-content'),
).map((node) => node.textContent);
expect(selected).toStrictEqual(['production', 'staging']);
});
});
});

View File

@@ -3,6 +3,8 @@ import {
getResourceDeploymentKeys,
} from 'hooks/useResourceAttribute/utils';
import { FeatureKeys } from '../../../../constants/features';
import { useAppContext } from '../../../../providers/App/App';
import { QueryChipContainer, QueryChipItem } from '../../styles';
import { IQueryChipProps } from './types';
@@ -11,7 +13,13 @@ function QueryChip({ queryData, onClose }: IQueryChipProps): JSX.Element {
onClose(queryData.id);
};
const isClosable = queryData.tagKey !== getResourceDeploymentKeys();
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const isClosable =
queryData.tagKey !== getResourceDeploymentKeys(dotMetricsEnabled);
return (
<QueryChipContainer>

View File

@@ -4,6 +4,8 @@ import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { ServiceMetricsProps } from '../types';
import { getQueryRangeRequestData } from '../utils';
import ServiceMetricTable from './ServiceMetricTable';
@@ -16,13 +18,19 @@ function ServiceMetricsApplication({
GlobalReducer
>((state) => state.globalTime);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const queryRangeRequestData = useMemo(
() =>
getQueryRangeRequestData({
topLevelOperations,
globalSelectedInterval,
dotMetricsEnabled,
}),
[globalSelectedInterval, topLevelOperations],
[globalSelectedInterval, topLevelOperations, dotMetricsEnabled],
);
return (
<ServiceMetricTable

View File

@@ -19,10 +19,13 @@ import {
export const serviceMetricsQuery = (
topLevelOperation: [keyof ServiceDataProps, string[]],
dotMetricsEnabled: boolean,
): QueryBuilderData => {
const p99AutoCompleteData: BaseAutocompleteData = {
dataType: DataTypes.Float64,
key: WidgetKeys.SignozLatencyBucket,
key: dotMetricsEnabled
? WidgetKeys.Signoz_latency_bucket
: WidgetKeys.Signoz_latency_bucket_norm,
type: '',
};
@@ -50,7 +53,9 @@ export const serviceMetricsQuery = (
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -73,7 +78,9 @@ export const serviceMetricsQuery = (
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -83,7 +90,7 @@ export const serviceMetricsQuery = (
id: '',
key: {
dataType: DataTypes.Int64,
key: WidgetKeys.StatusCode,
key: dotMetricsEnabled ? WidgetKeys.StatusCode : WidgetKeys.StatusCodeNorm,
type: MetricsType.Tag,
},
op: OPERATORS.IN,
@@ -106,7 +113,9 @@ export const serviceMetricsQuery = (
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -129,7 +138,9 @@ export const serviceMetricsQuery = (
id: '',
key: {
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Resource,
},
op: OPERATORS.IN,
@@ -182,7 +193,9 @@ export const serviceMetricsQuery = (
const groupBy: BaseAutocompleteData[] = [
{
dataType: DataTypes.String,
key: WidgetKeys.OTelServiceName,
key: dotMetricsEnabled
? WidgetKeys.Service_name
: WidgetKeys.Service_name_norm,
type: MetricsType.Tag,
},
];

View File

@@ -17,6 +17,8 @@ import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { Tags } from 'types/reducer/trace';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import SkipOnBoardingModal from '../SkipOnBoardModal';
import ServiceTraceTable from './ServiceTracesTable';
@@ -38,6 +40,11 @@ function ServiceTraces(): JSX.Element {
selectedTags,
});
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
useErrorNotification(error);
const services = data || [];
@@ -55,7 +62,7 @@ function ServiceTraces(): JSX.Element {
useEffect(() => {
if (!logEventCalledRef.current && !isUndefined(data)) {
const selectedEnvironments = queries.find(
(val) => val.tagKey === getResourceDeploymentKeys(),
(val) => val.tagKey === getResourceDeploymentKeys(dotMetricsEnabled),
)?.tagValue;
const rps = data.reduce((total, service) => total + service.callRate, 0);

View File

@@ -26,6 +26,7 @@ export interface ServiceMetricsTableProps {
export interface GetQueryRangeRequestDataProps {
topLevelOperations: [keyof ServiceDataProps, string[]][];
globalSelectedInterval: Time | CustomTimeType;
dotMetricsEnabled: boolean;
}
export interface GetServiceListFromQueryProps {

View File

@@ -26,6 +26,7 @@ export function getSeriesValue(
export const getQueryRangeRequestData = ({
topLevelOperations,
globalSelectedInterval,
dotMetricsEnabled,
}: GetQueryRangeRequestDataProps): GetQueryResultsProps[] => {
const requestData: GetQueryResultsProps[] = [];
topLevelOperations.forEach((operation) => {
@@ -33,7 +34,7 @@ export const getQueryRangeRequestData = ({
query: {
queryType: EQueryType.QUERY_BUILDER,
promql: [],
builder: serviceMetricsQuery(operation),
builder: serviceMetricsQuery(operation, dotMetricsEnabled),
clickhouse_sql: [],
id: uuid(),
},

View File

@@ -1,22 +1,19 @@
import { useCallback, useMemo } from 'react';
import type {
AuthtypesGettableRoleDTO,
AuthtypesUserRoleDTO,
} from 'api/generated/services/sigNoz.schemas';
import { useQueryClient } from 'react-query';
import type { AuthtypesGettableRoleDTO } from 'api/generated/services/sigNoz.schemas';
import {
useCreateUserRole,
useDeleteUserRole,
useGetUser,
getGetRolesByUserIDQueryKey,
useGetRolesByUserID,
useRemoveUserRoleByUserIDAndRoleID,
useSetRoleByUserID,
} from 'api/generated/services/users';
import { retryOn429 } from 'utils/errorUtils';
const enum PromiseStatus {
Fulfilled = 'fulfilled',
Rejected = 'rejected',
}
// Stable identity so the memos below do not recompute on every render.
const EMPTY_USER_ROLES: AuthtypesUserRoleDTO[] = [];
export interface MemberRoleUpdateFailure {
roleName: string;
error: unknown;
@@ -36,30 +33,30 @@ export function useMemberRoleManager(
userId: string,
enabled: boolean,
): UseMemberRoleManagerResult {
const { data, isLoading } = useGetUser(
const queryClient = useQueryClient();
const { data, isLoading } = useGetRolesByUserID(
{ id: userId },
{ query: { enabled: !!userId && enabled } },
);
const userRoles = data?.data?.userRoles ?? EMPTY_USER_ROLES;
const currentRoles = useMemo<AuthtypesGettableRoleDTO[]>(
() => userRoles.map((userRole) => userRole.role),
[userRoles],
() => data?.data ?? [],
[data?.data],
);
// DELETE /api/v2/user_roles/{id} is keyed by the user_role join row, not the role.
const assignmentIdByRoleId = useMemo(
() => new Map(userRoles.map((userRole) => [userRole.roleId, userRole.id])),
[userRoles],
);
const { mutateAsync: setRole } = useSetRoleByUserID({
mutation: { retry: retryOn429 },
});
const { mutateAsync: removeRole } = useRemoveUserRoleByUserIDAndRoleID({
mutation: { retry: retryOn429 },
});
const { mutateAsync: createUserRole } = useCreateUserRole({
mutation: { retry: retryOn429 },
});
const { mutateAsync: deleteUserRole } = useDeleteUserRole({
mutation: { retry: retryOn429 },
});
const invalidateRoles = useCallback(
() =>
queryClient.invalidateQueries(getGetRolesByUserIDQueryKey({ id: userId })),
[userId, queryClient],
);
const applyDiff = useCallback(
async (
@@ -83,33 +80,30 @@ export function useMemberRoleManager(
const allOperations = [
...addedRoles.map((role) => ({
role,
run: (): ReturnType<typeof createUserRole> =>
createUserRole({ data: { userId, roleId: role.id ?? '' } }),
run: (): ReturnType<typeof setRole> =>
setRole({
pathParams: { id: userId },
data: { name: role.name ?? '' },
}),
})),
...removedRoles.map((role) => ({
role,
run: (): ReturnType<typeof removeRole> =>
removeRole({ pathParams: { id: userId, roleId: role.id ?? '' } }),
})),
...removedRoles
.map((role) => ({
role,
assignmentId: assignmentIdByRoleId.get(role.id ?? ''),
}))
.filter(
(
entry,
): entry is {
role: AuthtypesGettableRoleDTO;
assignmentId: string;
} => !!entry.assignmentId,
)
.map(({ role, assignmentId }) => ({
role,
run: (): ReturnType<typeof deleteUserRole> =>
deleteUserRole({ pathParams: { id: assignmentId } }),
})),
];
const results = await Promise.allSettled(
allOperations.map((op) => op.run()),
);
const successCount = results.filter(
(r) => r.status === PromiseStatus.Fulfilled,
).length;
if (successCount > 0) {
await invalidateRoles();
}
const failures: MemberRoleUpdateFailure[] = [];
results.forEach((result, index) => {
if (result.status === PromiseStatus.Rejected) {
@@ -119,6 +113,7 @@ export function useMemberRoleManager(
error: result.reason,
onRetry: async (): Promise<void> => {
await run();
await invalidateRoles();
},
});
}
@@ -126,7 +121,7 @@ export function useMemberRoleManager(
return failures;
},
[userId, currentRoles, assignmentIdByRoleId, createUserRole, deleteUserRole],
[userId, currentRoles, setRole, removeRole, invalidateRoles],
);
return { currentRoles, isLoading, applyDiff };

View File

@@ -27,6 +27,7 @@ export type WhereClauseConfig = {
export const useAutoComplete = (
query: IBuilderQuery,
dotMetricsEnabled: boolean,
whereClauseConfig?: WhereClauseConfig,
shouldUseSuggestions?: boolean,
isInfraMonitoring?: boolean,
@@ -39,6 +40,7 @@ export const useAutoComplete = (
const { keys, results, isFetching, exampleQueries } = useFetchKeysAndValues(
searchValue,
query,
dotMetricsEnabled,
searchKey,
shouldUseSuggestions,
isInfraMonitoring,

View File

@@ -48,6 +48,7 @@ type IuseFetchKeysAndValues = {
export const useFetchKeysAndValues = (
searchValue: string,
query: IBuilderQuery,
dotMetricsEnabled: boolean,
searchKey: string,
shouldUseSuggestions?: boolean,
isInfraMonitoring?: boolean,

View File

@@ -1,10 +1,14 @@
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { QueryParams } from 'constants/query';
import ROUTES from 'constants/routes';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import { encode } from 'js-base64';
import { FeatureKeys } from '../../constants/features';
import { useAppContext } from '../../providers/App/App';
import { whilelistedKeys } from './config';
import { ResourceContext } from './context';
import {
IResourceAttribute,
@@ -54,6 +58,11 @@ function ResourceProvider({ children }: Props): JSX.Element {
}
};
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const dispatchQueries = useCallback(
(queries: IResourceAttribute[]): void => {
urlQuery.set(
@@ -69,7 +78,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
const loadTagKeys = (): void => {
handleLoading(true);
GetTagKeys()
GetTagKeys(dotMetricsEnabled)
.then((tagKeys) => {
const options = mappingWithRoutesAndKeys(pathname, tagKeys);
setOptionsData({ options, mode: undefined });
@@ -152,15 +161,15 @@ function ResourceProvider({ children }: Props): JSX.Element {
setSelectedQueries([...value]);
},
[optionsData.mode, step, staging, pathname],
[optionsData.mode, step, staging, dotMetricsEnabled, pathname],
);
const handleEnvironmentChange = useCallback(
(environments: string[]): void => {
const staging = [getResourceDeploymentKeys(), 'IN'];
const staging = [getResourceDeploymentKeys(dotMetricsEnabled), 'IN'];
const queriesCopy = queries.filter(
(query) => query.tagKey !== getResourceDeploymentKeys(),
(query) => query.tagKey !== getResourceDeploymentKeys(dotMetricsEnabled),
);
if (environments && Array.isArray(environments) && environments.length > 0) {
@@ -175,7 +184,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
setStep('Idle');
},
[dispatchQueries, queries],
[dispatchQueries, dotMetricsEnabled, queries],
);
const handleClose = useCallback(
@@ -193,9 +202,16 @@ function ResourceProvider({ children }: Props): JSX.Element {
setOptionsData({ mode: undefined, options: [] });
}, [dispatchQueries]);
const getVisibleQueries = useMemo(() => {
if (pathname === ROUTES.SERVICE_MAP) {
return queries.filter((query) => whilelistedKeys.includes(query.tagKey));
}
return queries;
}, [queries, pathname]);
const value: IResourceAttributeProps = useMemo(
() => ({
queries,
queries: getVisibleQueries,
staging,
handleClearAll,
handleClose,
@@ -218,7 +234,7 @@ function ResourceProvider({ children }: Props): JSX.Element {
staging,
selectedQuery,
optionsData,
queries,
getVisibleQueries,
],
);

View File

@@ -2,9 +2,13 @@ import { ReactNode } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Router } from 'react-router-dom';
import { act, renderHook, waitFor } from '@testing-library/react';
import { FeatureKeys } from 'constants/features';
import ROUTES from 'constants/routes';
import { createMemoryHistory, MemoryHistory } from 'history';
import { encode } from 'js-base64';
import { AppContext } from 'providers/App/App';
import { IAppContext } from 'providers/App/types';
import { getAppContextMock } from 'tests/test-utils';
import ResourceProvider from '../ResourceProvider';
import useResourceAttribute from '../useResourceAttribute';
@@ -51,8 +55,10 @@ const mockTagValues = getResourceAttributesTagValues as jest.MockedFunction<
function createWrapper({
routerHistory,
appContextOverrides,
}: {
routerHistory: MemoryHistory;
appContextOverrides?: Partial<IAppContext>;
}): ({ children }: { children: ReactNode }) => JSX.Element {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
@@ -60,9 +66,13 @@ function createWrapper({
return function Wrapper({ children }: { children: ReactNode }): JSX.Element {
return (
<QueryClientProvider client={queryClient}>
<Router history={routerHistory}>
<ResourceProvider>{children}</ResourceProvider>
</Router>
<AppContext.Provider
value={getAppContextMock('ADMIN', appContextOverrides)}
>
<Router history={routerHistory}>
<ResourceProvider>{children}</ResourceProvider>
</Router>
</AppContext.Provider>
</QueryClientProvider>
);
};
@@ -401,7 +411,7 @@ describe('ResourceProvider', () => {
});
describe('handleEnvironmentChange', () => {
it('adds a dotted environment query when envs are provided', async () => {
it('adds an environment query when envs are provided', async () => {
const routerHistory = createMemoryHistory({ initialEntries: ['/'] });
const { result } = renderHook(() => useResourceAttribute(), {
wrapper: createWrapper({ routerHistory }),
@@ -414,7 +424,7 @@ describe('ResourceProvider', () => {
await waitFor(() => {
expect(result.current.queries).toHaveLength(1);
expect(result.current.queries[0]).toMatchObject({
tagKey: 'resource_deployment.environment',
tagKey: 'resource_deployment_environment',
operator: 'IN',
tagValue: ['production'],
});
@@ -425,7 +435,7 @@ describe('ResourceProvider', () => {
const seeded = [
{
id: 'env',
tagKey: 'resource_deployment.environment',
tagKey: 'resource_deployment_environment',
operator: 'IN',
tagValue: ['production'],
},
@@ -449,7 +459,7 @@ describe('ResourceProvider', () => {
await waitFor(() => {
const tagKeys = result.current.queries.map((q) => q.tagKey);
expect(tagKeys).not.toContain('resource_deployment.environment');
expect(tagKeys).not.toContain('resource_deployment_environment');
expect(tagKeys).toContain('resource_service_name');
});
});
@@ -458,7 +468,7 @@ describe('ResourceProvider', () => {
const seeded = [
{
id: 'env',
tagKey: 'resource_deployment.environment',
tagKey: 'resource_deployment_environment',
operator: 'IN',
tagValue: ['production'],
},
@@ -476,13 +486,43 @@ describe('ResourceProvider', () => {
await waitFor(() => {
const envQueries = result.current.queries.filter(
(q) => q.tagKey === 'resource_deployment.environment',
(q) => q.tagKey === 'resource_deployment_environment',
);
expect(envQueries).toHaveLength(1);
expect(envQueries[0].tagValue).toStrictEqual(['staging']);
});
});
it('uses the dotted deployment env key when DOT_METRICS_ENABLED is active', async () => {
const routerHistory = createMemoryHistory({ initialEntries: ['/'] });
const { result } = renderHook(() => useResourceAttribute(), {
wrapper: createWrapper({
routerHistory,
appContextOverrides: {
featureFlags: [
{
name: FeatureKeys.DOT_METRICS_ENABLED,
active: true,
usage: 0,
usage_limit: -1,
route: '',
},
],
},
}),
});
act(() => {
result.current.handleEnvironmentChange(['production']);
});
await waitFor(() => {
expect(result.current.queries[0].tagKey).toBe(
'resource_deployment.environment',
);
});
});
it('preserves unrelated query params when dispatching', async () => {
const routerHistory = createMemoryHistory({
initialEntries: ['/?tab=overview'],
@@ -504,23 +544,22 @@ describe('ResourceProvider', () => {
});
});
describe('SERVICE_MAP', () => {
const seeded = [
{
id: 'a',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
},
{
id: 'b',
tagKey: 'resource_k8s_cluster_name',
operator: 'IN',
tagValue: ['prod'],
},
];
it('exposes every query from the URL, including ones the map cannot apply', () => {
describe('getVisibleQueries (SERVICE_MAP filtering)', () => {
it('filters queries down to whitelisted keys on SERVICE_MAP', () => {
const seeded = [
{
id: 'a',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
},
{
id: 'b',
tagKey: 'resource_k8s_cluster_name',
operator: 'IN',
tagValue: ['prod'],
},
];
mockLibHistory(
`?resourceAttribute=${encode(JSON.stringify(seeded))}`,
ROUTES.SERVICE_MAP,
@@ -533,10 +572,24 @@ describe('ResourceProvider', () => {
wrapper: createWrapper({ routerHistory }),
});
expect(result.current.queries).toStrictEqual(seeded);
expect(result.current.queries).toStrictEqual([seeded[1]]);
});
it('returns all queries on non-SERVICE_MAP routes', () => {
const seeded = [
{
id: 'a',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
},
{
id: 'b',
tagKey: 'resource_k8s_cluster_name',
operator: 'IN',
tagValue: ['prod'],
},
];
mockLibHistory(
`?resourceAttribute=${encode(JSON.stringify(seeded))}`,
'/services',

View File

@@ -1,20 +1,17 @@
import ROUTES from 'constants/routes';
import { whilelistedKeys } from '../config';
import {
filterServiceMapSupportedQueries,
mappingWithRoutesAndKeys,
} from '../utils';
import { mappingWithRoutesAndKeys } from '../utils';
describe('useResourceAttribute config', () => {
describe('whilelistedKeys', () => {
it('should include underscore-notation keys', () => {
it('should include underscore-notation keys (DOT_METRICS_ENABLED=false)', () => {
expect(whilelistedKeys).toContain('resource_deployment_environment');
expect(whilelistedKeys).toContain('resource_k8s_cluster_name');
expect(whilelistedKeys).toContain('resource_k8s_cluster_namespace');
});
it('should include dot-notation keys', () => {
it('should include dot-notation keys (DOT_METRICS_ENABLED=true)', () => {
expect(whilelistedKeys).toContain('resource_deployment.environment');
expect(whilelistedKeys).toContain('resource_k8s.cluster.name');
expect(whilelistedKeys).toContain('resource_k8s.cluster.namespace');
@@ -77,29 +74,4 @@ describe('useResourceAttribute config', () => {
expect(result).toStrictEqual(allFilters);
});
});
describe('filterServiceMapSupportedQueries', () => {
const environmentQuery = {
id: 'env',
tagKey: 'resource_deployment_environment',
operator: 'IN',
tagValue: ['production'],
};
const serviceQuery = {
id: 'svc',
tagKey: 'resource_service_name',
operator: 'IN',
tagValue: ['frontend'],
};
it('should keep only the queries the service map can filter on', () => {
expect(
filterServiceMapSupportedQueries([environmentQuery, serviceQuery]),
).toStrictEqual([environmentQuery]);
});
it('should return an empty list when no query is supported', () => {
expect(filterServiceMapSupportedQueries([serviceQuery])).toStrictEqual([]);
});
});
});

View File

@@ -144,11 +144,19 @@ export const OperatorSchema: IOption[] = OperatorConversions.map(
}),
);
export const getResourceDeploymentKeys = (): string =>
'resource_deployment.environment';
export const getResourceDeploymentKeys = (
dotMetricsEnabled: boolean,
): string => {
if (dotMetricsEnabled) {
return 'resource_deployment.environment';
}
return 'resource_deployment_environment';
};
export const GetTagKeys = async (): Promise<IOption[]> => {
const resourceDeploymentKey = getResourceDeploymentKeys();
export const GetTagKeys = async (
dotMetricsEnabled: boolean,
): Promise<IOption[]> => {
const resourceDeploymentKey = getResourceDeploymentKeys(dotMetricsEnabled);
const { payload } = await getResourceAttributesTagKeys({
metricName: 'signoz_calls_total',
match: 'resource_',
@@ -168,10 +176,12 @@ export const GetTagKeys = async (): Promise<IOption[]> => {
}));
};
export const getEnvironmentTagKeys = async (): Promise<IOption[]> => {
export const getEnvironmentTagKeys = async (
dotMetricsEnabled: boolean,
): Promise<IOption[]> => {
const { payload } = await getResourceAttributesTagKeys({
metricName: 'signoz_calls_total',
match: getResourceDeploymentKeys(),
match: getResourceDeploymentKeys(dotMetricsEnabled),
});
if (!payload || !payload?.data) {
return [];
@@ -184,9 +194,11 @@ export const getEnvironmentTagKeys = async (): Promise<IOption[]> => {
}));
};
export const getEnvironmentTagValues = async (): Promise<IOption[]> => {
export const getEnvironmentTagValues = async (
dotMetricsEnabled: boolean,
): Promise<IOption[]> => {
const { payload } = await getResourceAttributesTagValues({
tagKey: getResourceDeploymentKeys(),
tagKey: getResourceDeploymentKeys(dotMetricsEnabled),
metricName: 'signoz_calls_total',
});
@@ -281,8 +293,3 @@ export const mappingWithRoutesAndKeys = (
}
return filters;
};
export const filterServiceMapSupportedQueries = (
queries: IResourceAttribute[],
): IResourceAttribute[] =>
queries.filter((query) => whilelistedKeys.includes(query.tagKey));

View File

@@ -19,30 +19,6 @@ type CompositeWithBuilder = {
builder?: { queryData?: IBuilderQuery[] };
};
export function saveRecentQueryByExpression(
dataSource: IBuilderQuery['dataSource'],
expression: string | null | undefined,
source = '',
): void {
const trimmed = expression?.trim();
if (!trimmed) {
return;
}
const validation = validateQuery(trimmed);
if (!validation.isValid) {
return;
}
const signal = toSignal(dataSource);
if (!signal) {
return;
}
store.save({
signal,
source,
filter: { expression: trimmed },
});
}
// Persists each builder query in the composite as a recent entry. Call this
// only from explicit user-driven Run triggers — reacting to stagedQuery or any
// other derived state pollutes recents with navigation/refresh/go-to traffic.
@@ -55,10 +31,22 @@ export function saveRecentQuery(
}
queryData.forEach((q) => {
saveRecentQueryByExpression(
q.dataSource,
q.filter?.expression,
q.source ?? '',
);
const expression = q.filter?.expression?.trim();
if (!expression) {
return;
}
const validation = validateQuery(expression);
if (!validation.isValid) {
return;
}
const signal = toSignal(q.dataSource);
if (!signal) {
return;
}
store.save({
signal,
source: q.source ?? '',
filter: q.filter ?? { expression: '' },
});
});
}

View File

@@ -42,6 +42,7 @@ export class UPlotScaleBuilder extends ConfigBuilder<
logBase = 10,
padMinBy = 0,
padMaxBy = 0.05,
useExactTimeRange = false,
} = this.props;
// Special handling for time scales (X axis)
@@ -58,14 +59,20 @@ export class UPlotScaleBuilder extends ConfigBuilder<
// Align max time to "endTime - 1 minute", rounded down to minute precision
// This matches legacy getXAxisScale behavior and avoids empty space at the right edge
const oneMinuteAgoTimestamp = (maxTime - 60) * 1000;
const currentDate = new Date(oneMinuteAgoTimestamp);
if (!useExactTimeRange) {
const oneMinuteAgoTimestamp = (maxTime - 60) * 1000;
const currentDate = new Date(oneMinuteAgoTimestamp);
currentDate.setSeconds(0);
currentDate.setMilliseconds(0);
currentDate.setSeconds(0);
currentDate.setMilliseconds(0);
const unixTimestampSeconds = Math.floor(currentDate.getTime() / 1000);
maxTime = unixTimestampSeconds;
const unixTimestampSeconds = Math.floor(currentDate.getTime() / 1000);
// Trimming past min inverts the range, which uPlot draws as an empty plot.
if (unixTimestampSeconds > minTime) {
maxTime = unixTimestampSeconds;
}
}
return {
[scaleKey]: {

View File

@@ -79,6 +79,44 @@ describe('UPlotScaleBuilder', () => {
expect(resolvedMax).toBe(expectedMax);
});
it('plots min/max as given when useExactTimeRange is set', () => {
const min = 1_700_000_000;
const max = 1_700_000_630;
const builder = new UPlotScaleBuilder(
createScaleProps({
scaleKey: 'x',
time: true,
min,
max,
useExactTimeRange: true,
}),
);
const config = builder.getConfig();
expect(config.x.range).toStrictEqual([min, max]);
});
it('keeps the requested end when the window is shorter than the trim', () => {
// 23 second window: trimming a minute off the end would put max before min.
const min = 1_786_527_160;
const max = 1_786_527_183;
const builder = new UPlotScaleBuilder(
createScaleProps({
scaleKey: 'x',
time: true,
min,
max,
}),
);
const config = builder.getConfig();
expect(config.x.range).toStrictEqual([min, max]);
});
it('falls back to getFallbackMinMaxTimeStamp when time scale has no min/max', () => {
getFallbackMinMaxSpy.mockReturnValue({
fallbackMin: 100,

View File

@@ -97,6 +97,8 @@ export interface ScaleProps {
auto?: boolean;
logBase?: uPlot.Scale.LogBase;
distribution?: DistributionType;
/** Plots a time scale's `min`/`max` as given, skipping the trim below. */
useExactTimeRange?: boolean;
}
export enum DisconnectedValuesMode {

View File

@@ -0,0 +1,218 @@
export const membersResponse = [
{
id: '3223a874-5678458745786',
name: 'John Doe',
email: 'firstUser@test.io',
createdAt: 1666357530,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '5e9681b1-5678458745786',
name: 'Jane Doe',
email: 'johndoe2@test.io',
createdAt: 1666365394,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '11e8c55d-5678458745786',
name: 'Alex',
email: 'blah@test.io',
createdAt: 1666366317,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: 'd878012367813286731aab62',
role: 'VIEWER',
organization: 'Test Inc',
flags: null,
},
{
id: '2ad2e404-5678458745786',
name: 'Tom',
email: 'johndoe4@test.io',
createdAt: 1673441483,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '6f532456-5678458745786',
name: 'Harry',
email: 'harry@test.io',
createdAt: 1691551672,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: 'ae22fa73-5678458745786',
name: 'Ron',
email: 'ron@test.io',
createdAt: 1691668239,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '3223a874-5678458745786',
name: 'John Doe',
email: 'johndoe@test.io',
createdAt: 1666357530,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '5e9681b1-5678458745786',
name: 'Jane Doe',
email: 'johndoe2@test.io',
createdAt: 1666365394,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '11e8c55d-5678458745786',
name: 'Alex',
email: 'blah@test.io',
createdAt: 1666366317,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: 'd878012367813286731aab62',
role: 'VIEWER',
organization: 'Test Inc',
flags: null,
},
{
id: '2ad2e404-5678458745786',
name: 'Tom',
email: 'johndoe4@test.io',
createdAt: 1673441483,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '6f532456-5678458745786',
name: 'Harry',
email: 'harry@test.io',
createdAt: 1691551672,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: 'ae22fa73-5678458745786',
name: 'Ron',
email: 'ron@test.io',
createdAt: 1691668239,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '3223a874-5678458745786',
name: 'John Doe',
email: 'johndoe@test.io',
createdAt: 1666357530,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '5e9681b1-5678458745786',
name: 'Jane Doe',
email: 'johndoe2@test.io',
createdAt: 1666365394,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '11e8c55d-5678458745786',
name: 'Alex',
email: 'blah@test.io',
createdAt: 1666366317,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: 'd878012367813286731aab62',
role: 'VIEWER',
organization: 'Test Inc',
flags: null,
},
{
id: '2ad2e404-5678458745786',
name: 'Tom',
email: 'johndoe4@test.io',
createdAt: 1673441483,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: '6f532456-5678458745786',
name: 'Harry',
email: 'harry@test.io',
createdAt: 1691551672,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
{
id: 'ae22fa73-5678458745786',
name: 'Ron',
email: 'lastUser@test.io',
createdAt: 1691668239,
profilePictureURL: '',
orgId: '1287612376312867312867',
groupId: '5678458745786',
role: 'ADMIN',
organization: 'Test Inc',
flags: null,
},
];

View File

@@ -12,6 +12,7 @@ import {
} from './__mockdata__/dashboards';
import { explorerView } from './__mockdata__/explorer_views';
import { licensesSuccessResponse } from './__mockdata__/licenses';
import { membersResponse } from './__mockdata__/members';
import { queryRangeSuccessResponse } from './__mockdata__/query_range';
import { serviceSuccessResponse } from './__mockdata__/services';
import { topLevelOperationSuccessResponse } from './__mockdata__/top_level_operations';
@@ -39,6 +40,9 @@ export const handlers = [
res(ctx.status(200), ctx.json(topLevelOperationSuccessResponse)),
),
rest.get('http://localhost/api/v1/user', (req, res, ctx) =>
res(ctx.status(200), ctx.json({ status: '200', data: membersResponse })),
),
rest.get(
'http://localhost/api/v3/autocomplete/attribute_keys',
(req, res, ctx) => {
@@ -160,6 +164,15 @@ export const handlers = [
),
),
rest.post('http://localhost/api/v1/invite', (_, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: 'invite sent successfully',
}),
),
),
rest.get(
'http://localhost/api/v3/autocomplete/aggregate_attributes',
(req, res, ctx) =>

View File

@@ -1,6 +1,6 @@
//@ts-nocheck
import { useEffect, useMemo, useRef } from 'react';
import { useEffect, useRef } from 'react';
// eslint-disable-next-line no-restricted-imports
import { connect } from 'react-redux';
import { RouteComponentProps, withRouter } from 'react-router-dom';
@@ -11,7 +11,6 @@ import ResourceAttributesFilter from 'container/ResourceAttributesFilter';
import useResourceAttribute from 'hooks/useResourceAttribute';
import { whilelistedKeys } from 'hooks/useResourceAttribute/config';
import { IResourceAttribute } from 'hooks/useResourceAttribute/types';
import { filterServiceMapSupportedQueries } from 'hooks/useResourceAttribute/utils';
import { getDetailedServiceMapItems, ServiceMapStore } from 'store/actions';
import { AppState } from 'store/reducers';
import styled from 'styled-components';
@@ -71,37 +70,32 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
const { queries } = useResourceAttribute();
const supportedQueries = useMemo(
() => filterServiceMapSupportedQueries(queries),
[queries],
);
useEffect(() => {
/*
Call the apis only when the route is loaded.
Check this issue: https://github.com/SigNoz/signoz/issues/110
*/
getDetailedServiceMapItems(globalTime, supportedQueries);
}, [globalTime, getDetailedServiceMapItems, supportedQueries]);
getDetailedServiceMapItems(globalTime, queries);
}, [globalTime, getDetailedServiceMapItems, queries]);
useEffect(() => {
fgRef.current && fgRef.current.d3Force('charge').strength(-400);
});
const renderBody = (): JSX.Element => {
if (serviceMap.loading) {
return <Spinner size="large" tip="Loading..." />;
}
if (serviceMap.items.length === 0) {
return <Card>No Service Found</Card>;
}
return <Map fgRef={fgRef} serviceMap={serviceMap} />;
};
if (serviceMap.loading) {
return <Spinner size="large" tip="Loading..." />;
}
if (!serviceMap.loading && serviceMap.items.length === 0) {
return (
<Container>
<ResourceAttributesFilter />
<Card>No Service Found</Card>
</Container>
);
}
return (
<Container className="service-map-container">
<div className="service-map-container">
<ResourceAttributesFilter
suffixIcon={
<TextToolTip
@@ -114,8 +108,8 @@ function ServiceMap(props: ServiceMapProps): JSX.Element {
}
/>
{renderBody()}
</Container>
<Map fgRef={fgRef} serviceMap={serviceMap} />
</div>
);
}

View File

@@ -182,56 +182,4 @@ describe('ValueSelector', () => {
});
});
});
describe('opening and closing without touching the list', () => {
function renderWith(
selection: VariableSelection,
options: string[],
): jest.Mock {
const onChange = jest.fn();
render(
<TooltipProvider>
<ValueSelector
options={options}
variableType="dynamic"
multiSelect
showAllOption
selection={selection}
onChange={onChange}
emptyFallback={{ value: [], allSelected: false }}
testId="variable-select-env"
/>
</TooltipProvider>,
);
return onChange;
}
async function openThenClose(): Promise<void> {
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
const control = screen.getByTestId('variable-select-env');
await user.click(control.querySelector('input') as HTMLInputElement);
await user.keyboard('{Escape}');
}
it('does not promote a pick that covers every available option to ALL', async () => {
// A narrow time range can leave only the selected value in the list. That is
// still an explicit pick, not "everything, always".
const onChange = renderWith(
{ value: ['checkout-service-prod'], allSelected: false },
['checkout-service-prod'],
);
await openThenClose();
expect(onChange).not.toHaveBeenCalled();
});
it('does not rewrite a dynamic ALL into concrete values', async () => {
const onChange = renderWith({ value: null, allSelected: true }, OPTIONS);
await openThenClose();
expect(onChange).not.toHaveBeenCalled();
});
});
});

View File

@@ -145,133 +145,6 @@ describe('reconcileWithOptions', () => {
),
).toBeNull();
});
describe('preserveSelection (options moved on their own — time range, reload)', () => {
const multi = model({
type: 'DYNAMIC',
multiSelect: true,
showAllOption: true,
dynamicAttribute: 'service.name',
});
it('keeps a multi-select pick the new option list no longer offers', () => {
expect(
reconcileWithOptions(multi, { value: ['frontend'], allSelected: false }, [
'backend',
'cart',
]),
).toStrictEqual({ value: null, allSelected: true });
expect(
reconcileWithOptions(
multi,
{ value: ['frontend'], allSelected: false },
['backend', 'cart'],
{ preserveSelection: true },
),
).toBeNull();
});
it('still materializes ALL, which must track the option list', () => {
expect(
reconcileWithOptions(
model({ type: 'QUERY', multiSelect: true, showAllOption: true }),
{ value: ['a'], allSelected: true },
['a', 'b'],
{ preserveSelection: true },
),
).toStrictEqual({ value: ['a', 'b'], allSelected: true });
});
it('still fills the default when nothing is selected yet', () => {
expect(
reconcileWithOptions(multi, { value: [], allSelected: false }, ['a', 'b'], {
preserveSelection: true,
}),
).toStrictEqual({ value: null, allSelected: true });
});
});
// A typed value is in no option list, so no refetch can invalidate it.
describe('customValues (typed in, never offered by the data)', () => {
const multi = model({
type: 'DYNAMIC',
multiSelect: true,
showAllOption: true,
dynamicAttribute: 'service.name',
});
it('keeps them through a re-scope that drops a fetched value', () => {
expect(
reconcileWithOptions(
multi,
{
value: ['frontend', 'typed-in'],
allSelected: false,
customValues: ['typed-in'],
},
['backend', 'cart'],
),
).toStrictEqual({
value: ['typed-in'],
allSelected: false,
customValues: ['typed-in'],
});
});
it('never re-defaults a selection made only of them', () => {
expect(
reconcileWithOptions(
multi,
{ value: ['typed-in'], allSelected: false, customValues: ['typed-in'] },
['backend', 'cart'],
),
).toBeNull();
});
// An inert marker is not worth a store write + dependent refetch to prune.
it('leaves a stale marker alone when it drops nothing', () => {
expect(
reconcileWithOptions(
multi,
{
value: ['frontend', 'typed-in'],
allSelected: false,
customValues: ['typed-in', 'removed-earlier'],
},
['frontend'],
),
).toBeNull();
});
it('prunes markers for values it does drop', () => {
expect(
reconcileWithOptions(
multi,
{
value: ['stale', 'typed-in'],
allSelected: false,
customValues: ['typed-in'],
},
['frontend'],
),
).toStrictEqual({
value: ['typed-in'],
allSelected: false,
customValues: ['typed-in'],
});
});
it('still drops an unmarked value the list no longer offers', () => {
expect(
reconcileWithOptions(
multi,
{ value: ['frontend', 'stale'], allSelected: false },
['frontend'],
),
).toStrictEqual({ value: ['frontend'], allSelected: false });
});
});
});
describe('configuredDefaultValue', () => {

View File

@@ -1,91 +0,0 @@
import type { VariableSelection } from '../selectionTypes';
import { selectionFromCommittedValues } from '../utils/selectionUtils';
const OPTIONS = ['checkout', 'payments', 'cart'];
const FALLBACK: VariableSelection = { value: null, allSelected: true };
function commit(
values: string[],
overrides: Partial<Parameters<typeof selectionFromCommittedValues>[0]> = {},
): VariableSelection {
return selectionFromCommittedValues({
values,
options: OPTIONS,
showAllOption: true,
emptyFallback: FALLBACK,
...overrides,
});
}
// What a multi-select commit resolves to. The option list is known only here, so this
// is the one place a typed value can be recognised.
describe('selectionFromCommittedValues', () => {
it('marks values the option list did not offer as typed in', () => {
expect(commit(['checkout', 'typed-in'])).toStrictEqual({
value: ['checkout', 'typed-in'],
allSelected: false,
customValues: ['typed-in'],
});
});
it('marks a selection made only of typed-in values', () => {
expect(commit(['a', 'b'])).toStrictEqual({
value: ['a', 'b'],
allSelected: false,
customValues: ['a', 'b'],
});
});
it('records no marker when every pick came from the list', () => {
expect(commit(['checkout', 'cart'])).toStrictEqual({
value: ['checkout', 'cart'],
allSelected: false,
});
});
it('reads a set covering every option as ALL', () => {
expect(commit(OPTIONS)).toStrictEqual({
value: OPTIONS,
allSelected: true,
});
});
// ALL re-materializes to the option set, so recording this as ALL would drop the
// typed value on the next refetch.
it('does not read every option PLUS a typed value as ALL', () => {
expect(commit([...OPTIONS, 'typed-in'])).toStrictEqual({
value: [...OPTIONS, 'typed-in'],
allSelected: false,
customValues: ['typed-in'],
});
});
// Derived from the values + options at commit time, never from the old selection.
it('recomputes the marker: a typed value the data now offers is a normal pick', () => {
expect(
commit(['checkout', 'was-typed'], {
options: [...OPTIONS, 'was-typed'],
}),
).toStrictEqual({ value: ['checkout', 'was-typed'], allSelected: false });
});
it('does not read it as ALL when the variable offers no ALL', () => {
expect(commit(OPTIONS, { showAllOption: false })).toStrictEqual({
value: OPTIONS,
allSelected: false,
});
});
it('resolves an empty commit to the variable fallback', () => {
expect(commit([])).toBe(FALLBACK);
});
it('marks everything while the options have not arrived', () => {
// Nothing to judge against yet; erring this way keeps a value rather than dropping it.
expect(commit(['typed-in'], { options: [] })).toStrictEqual({
value: ['typed-in'],
allSelected: false,
customValues: ['typed-in'],
});
});
});

View File

@@ -4,8 +4,6 @@ import {
emptyVariableFormModel,
type VariableFormModel,
} from '../../DashboardSettings/Variables/variableFormModel';
import { VariableCycleReason } from '../../store/slices/variableFetchSlice';
import { useDashboardStore } from '../../store/useDashboardStore';
import type { VariableSelection } from '../selectionTypes';
import { useAutoSelect } from '../hooks/useAutoSelect';
@@ -17,11 +15,7 @@ function run(
variable: VariableFormModel,
options: string[],
selection: VariableSelection,
cycleReason?: VariableCycleReason,
): VariableSelection | undefined {
useDashboardStore.setState({
variableCycleReasons: cycleReason ? { [variable.name]: cycleReason } : {},
});
const onAutoSelect = jest.fn();
renderHook(() => useAutoSelect(variable, options, selection, onAutoSelect));
return onAutoSelect.mock.calls[0]?.[0];
@@ -76,13 +70,11 @@ describe('useAutoSelect', () => {
expect(next).toStrictEqual({ value: ['a', 'b'], allSelected: true });
});
// Re-scoped options only — a time-range refetch must NOT re-default; see below.
it('re-scoped: falls back to ALL, not the first option, when every selected value is gone', () => {
it('falls back to ALL, not the first option, when every selected value is gone', () => {
const next = run(
model({ type: 'QUERY', multiSelect: true, showAllOption: true }),
['x', 'y'],
{ value: ['a', 'b'], allSelected: false },
VariableCycleReason.ValueCascade,
);
expect(next).toStrictEqual({ value: ['x', 'y'], allSelected: true });
});
@@ -110,23 +102,20 @@ describe('useAutoSelect', () => {
expect(next).toStrictEqual({ value: ['b'], allSelected: false });
});
it('re-scoped: keeps the still-valid subset of a multi-select', () => {
it('keeps the still-valid subset of a multi-select when options re-scope', () => {
const next = run(
model({ type: 'QUERY', multiSelect: true }),
['a', 'b', 'd'],
{ value: ['a', 'b', 'c'], allSelected: false },
VariableCycleReason.ValueCascade,
);
expect(next).toStrictEqual({ value: ['a', 'b'], allSelected: false });
});
it('re-scoped: re-defaults a multi-select when none of the selected values remain', () => {
const next = run(
model({ type: 'QUERY', multiSelect: true }),
['x', 'y'],
{ value: ['a', 'b'], allSelected: false },
VariableCycleReason.ValueCascade,
);
it('re-defaults a multi-select when none of the selected values remain', () => {
const next = run(model({ type: 'QUERY', multiSelect: true }), ['x', 'y'], {
value: ['a', 'b'],
allSelected: false,
});
expect(next).toStrictEqual({ value: ['x'], allSelected: false });
});
@@ -162,45 +151,4 @@ describe('useAutoSelect', () => {
});
expect(next).toBeUndefined();
});
describe('by cycle reason', () => {
const service = model({
name: 'service',
type: 'DYNAMIC',
multiSelect: true,
showAllOption: true,
dynamicAttribute: 'service.name',
});
const gone: VariableSelection = { value: ['frontend'], allSelected: false };
it('keeps the selection when a full cycle refetched the options', () => {
// The new window has no data for the selected service — no reason to widen to ALL.
const next = run(
service,
['backend', 'cart'],
gone,
VariableCycleReason.FullCycle,
);
expect(next).toBeUndefined();
});
it('re-scopes the selection when a value cascade refetched the options', () => {
const next = run(
service,
['backend', 'cart'],
gone,
VariableCycleReason.ValueCascade,
);
expect(next).toStrictEqual({ value: null, allSelected: true });
});
it('reconciles a variable with no cycle of its own (custom definition change)', () => {
const next = run(
model({ name: 'env', type: 'CUSTOM', multiSelect: true }),
['staging', 'prod'],
{ value: ['dev'], allSelected: false },
);
expect(next).toStrictEqual({ value: ['staging'], allSelected: false });
});
});
});

View File

@@ -13,11 +13,11 @@ jest.mock('nuqs', () => ({
useQueryState: (): unknown => [null, jest.fn()],
}));
const mockGlobalTime = { minTime: 1, maxTime: 2, selectedTime: '5m' };
jest.mock('react-redux', () => ({
useSelector: (selector: (state: unknown) => unknown): unknown =>
selector({ globalTime: mockGlobalTime }),
selector({
globalTime: { minTime: 1, maxTime: 2, selectedTime: '5m' },
}),
}));
jest.mock('../../DashboardSettings/Variables/variableAdapters', () => ({
@@ -150,57 +150,3 @@ describe('useVariableSelection — setSelection', () => {
expect(svcCycleId()).toBe(before + 1);
});
});
describe('useVariableSelection — what a time-range change enqueues', () => {
// Longer than FETCH_CYCLE_DEBOUNCE_MS, which the hook keeps private.
const PAST_DEBOUNCE = 400;
function reasons(): Record<string, string> {
return useDashboardStore.getState().variableCycleReasons;
}
beforeEach(() => {
jest.useFakeTimers();
mockGlobalTime.selectedTime = '5m';
useDashboardStore.setState({
variableValues: {},
variableFetchStates: {},
variableLastUpdated: {},
variableCycleIds: {},
variableCycleReasons: {},
variableResolvedEmpty: {},
variableFetchContext: null,
lastFetchAllKey: null,
});
});
afterEach(() => {
jest.useRealTimers();
});
// The tag is what stops the reconcile re-defaulting a user's selection.
it('tags every variable as a full cycle, overriding an earlier cascade tag', () => {
const { result, rerender } = renderHook(() =>
useVariableSelection(dashboard),
);
act(() => {
jest.advanceTimersByTime(PAST_DEBOUNCE);
});
expect(reasons()).toStrictEqual({ env: 'full-cycle', svc: 'full-cycle' });
// A value change re-scopes the dependent's options: it may drop what no longer applies.
act(() => {
result.current.setSelection('env', { value: ['prod'], allSelected: false });
});
expect(reasons().svc).toBe('value-cascade');
mockGlobalTime.selectedTime = '30m';
rerender();
act(() => {
jest.advanceTimersByTime(PAST_DEBOUNCE);
});
expect(reasons()).toStrictEqual({ env: 'full-cycle', svc: 'full-cycle' });
});
});

View File

@@ -6,7 +6,6 @@ import { DashboardDetailEvents } from 'pages/DashboardPageV2/constants/events';
import type { VariableSelection } from '../../selectionTypes';
import { areSelectionsEqual } from '../../utils/resolveVariableSelection';
import { selectionFromCommittedValues } from '../../utils/selectionUtils';
import OverflowValuesTooltip from './OverflowValuesTooltip';
import styles from '../../VariablesBar.module.scss';
@@ -76,23 +75,13 @@ function ValueSelector({
options.every((option) => draft.includes(option));
const commit = (values: string[]): void => {
// A close that left the list as it opened commits nothing — else a pick covering
// every option this window offers would be promoted to a standing ALL.
if (
areSelectionsEqual(
{ value: values, allSelected: false },
{ value: committedValues, allSelected: false },
)
) {
return;
}
const next = selectionFromCommittedValues({
values,
options,
showAllOption,
emptyFallback,
});
// CustomMultiSelect emits the full value set when ALL is picked.
const isAll =
showAllOption &&
options.length > 0 &&
options.every((option) => values.includes(option));
const next: VariableSelection =
values.length === 0 ? emptyFallback : { value: values, allSelected: isAll };
// Closing without actually changing the selection must not re-fire onChange —
// that would needlessly re-cascade to dependent variables/panels.

View File

@@ -1,11 +1,6 @@
import { useEffect } from 'react';
import type { VariableFormModel } from '../../DashboardSettings/Variables/variableFormModel';
import {
selectVariableCycleReason,
VariableCycleReason,
} from '../../store/slices/variableFetchSlice';
import { useDashboardStore } from '../../store/useDashboardStore';
import { reconcileWithOptions } from '../utils/resolveVariableSelection';
import type { VariableSelection } from '../selectionTypes';
@@ -14,9 +9,6 @@ import type { VariableSelection } from '../selectionTypes';
* `onAutoSelect` only when the value must change. The reconcile rule lives in
* {@link reconcileWithOptions} (shared with seed + payload defaulting) so the bar
* and the panel query can never disagree about a variable's default.
*
* Only a value cascade may re-default the selection; a full cycle (time range,
* reload) leaves the user's pick alone. Types with no cycle of their own reconcile.
*/
export function useAutoSelect(
variable: VariableFormModel,
@@ -24,14 +16,8 @@ export function useAutoSelect(
selection: VariableSelection,
onAutoSelect: (selection: VariableSelection) => void,
): void {
const cycleReason = useDashboardStore(
selectVariableCycleReason(variable.name),
);
useEffect(() => {
const next = reconcileWithOptions(variable, selection, options, {
preserveSelection: cycleReason === VariableCycleReason.FullCycle,
});
const next = reconcileWithOptions(variable, selection, options);
if (next) {
onAutoSelect(next);
}

View File

@@ -10,11 +10,6 @@ export interface VariableSelection {
value: SelectedVariableValue;
/** True when every option is selected ("ALL"); for dynamic vars value may be null. */
allSelected: boolean;
/**
* Entries of `value` the user typed rather than picked. Never in any option list,
* so the reconcile keeps them instead of reading them as invalid.
*/
customValues?: string[];
}
/** Selected values for a dashboard's variables, keyed by variable name. */

View File

@@ -134,23 +134,12 @@ export function resolveDefaultSelection(
return { value: model.multiSelect ? [] : '', allSelected: false };
}
interface ReconcileOptions {
/**
* Set when no other variable caused this refetch (time-range change, reload): the
* selection then outranks the options and is kept as-is. Leave false for a
* dependency cascade, where a selection that no longer applies must give way.
*/
preserveSelection?: boolean;
}
/**
* Reconciles a variable's current selection against its freshly-fetched options.
* Returns the next selection, or null when nothing should change (a valid pick is
* left untouched — local-first). Behaviour, in order:
* - materialize ALL to the full option set (query/custom);
* - keep a multi-select selection outright when `preserveSelection` is set;
* - keep a still-valid multi-select subset, dropping only entries the list no longer
* offers and the user did not type in (`customValues`);
* - keep a still-valid multi-select subset, dropping only invalid entries;
* - otherwise auto-pick the default (or first option) so dependent variables and
* panels always resolve against a usable value.
*/
@@ -158,7 +147,6 @@ export function reconcileWithOptions(
model: VariableFormModel,
current: VariableSelection,
options: string[],
{ preserveSelection = false }: ReconcileOptions = {},
): VariableSelection | null {
if (options.length === 0) {
return null;
@@ -173,31 +161,13 @@ export function reconcileWithOptions(
Array.isArray(current.value) &&
current.value.length > 0
) {
// A pick this window has no data for is still the user's filter; re-defaulting it
// here is what widened a single pick to ALL on every time-range change.
if (preserveSelection) {
return null;
}
// A typed value is in no option list, so it is never "no longer offered".
const custom = new Set(current.customValues ?? []);
const valid = current.value
.map(String)
.filter((c) => options.includes(c) || custom.has(c));
const valid = current.value.map(String).filter((c) => options.includes(c));
if (valid.length === current.value.length) {
return null;
}
if (valid.length === 0) {
return fillDefault(model, options);
}
const customValues = valid.filter((v) => custom.has(v));
return {
value: valid,
allSelected: false,
...(customValues.length > 0 && { customValues }),
};
return valid.length > 0
? { value: valid, allSelected: false }
: fillDefault(model, options);
}
if (!model.multiSelect) {

View File

@@ -47,43 +47,6 @@ export function hasUsableValue(
return value !== '' && value !== null && value !== undefined;
}
interface CommittedValues {
values: string[];
options: string[];
showAllOption: boolean;
emptyFallback: VariableSelection;
}
/**
* The selection a multi-select commit resolves to. Options are known only here, so
* this is where a value the list never offered is recorded as typed in.
*/
export function selectionFromCommittedValues({
values,
options,
showAllOption,
emptyFallback,
}: CommittedValues): VariableSelection {
if (values.length === 0) {
return emptyFallback;
}
const customValues = values.filter((value) => !options.includes(value));
// ALL re-materializes to the option set, so a set carrying a typed value is not ALL
// — the next refetch would expand it back and drop what the user typed.
const allSelected =
showAllOption &&
options.length > 0 &&
customValues.length === 0 &&
options.every((option) => values.includes(option));
return {
value: values,
allSelected,
...(customValues.length > 0 && { customValues }),
};
}
/** Flatten the selection map into the `{ name: value }` payload a query expects. */
export function selectionToPayload(
selection: VariableSelectionMap,

View File

@@ -34,7 +34,6 @@ function reset(names: string[], context: VariableFetchContext): void {
variableFetchStates: {},
variableLastUpdated: {},
variableCycleIds: {},
variableCycleReasons: {},
variableFetchContext: null,
});
store().initVariableFetch(names, context);
@@ -134,33 +133,6 @@ describe('variableFetchSlice', () => {
expect(states().q1).toBe('error');
expect(states().q2).toBe('idle');
});
// The reason is what tells the post-fetch reconcile whether it may re-default a
// selection: a full cycle must not, a value cascade must.
it('tags a full cycle, then re-tags only the cascaded variables', () => {
store().enqueueFetchAll();
expect(store().variableCycleReasons).toStrictEqual({
q1: 'full-cycle',
q2: 'full-cycle',
d1: 'full-cycle',
d2: 'full-cycle',
});
resolve('q1');
store().enqueueDescendants('q1');
expect(store().variableCycleReasons).toStrictEqual({
q1: 'full-cycle',
q2: 'value-cascade',
d1: 'full-cycle',
d2: 'full-cycle',
});
});
it('drops the reason for a variable that no longer exists', () => {
store().enqueueFetchAll();
store().initVariableFetch(['q1'], context);
expect(store().variableCycleReasons).toStrictEqual({ q1: 'full-cycle' });
});
});
describe('variableFetchSlice — query depends on a dynamic', () => {

View File

@@ -9,7 +9,6 @@ import {
type FetchMaps,
isVariableInActiveFetchState,
resolveFetchState,
VariableCycleReason,
VariableFetchState,
} from './variableFetchSlice.utils';
@@ -31,10 +30,7 @@ function queryParentsHaveValues(
);
}
export {
VariableCycleReason,
VariableFetchState,
} from './variableFetchSlice.utils';
export { VariableFetchState } from './variableFetchSlice.utils';
/**
* Runtime fetch orchestration for dashboard variables — native port of V1's
@@ -49,8 +45,6 @@ export interface VariableFetchSlice {
variableFetchStates: Record<string, VariableFetchState>;
variableLastUpdated: Record<string, number>;
variableCycleIds: Record<string, number>;
/** Why each variable's current cycle was enqueued, read by the post-fetch reconcile. */
variableCycleReasons: Record<string, VariableCycleReason>;
/**
* Whether a QUERY/DYNAMIC variable settled its fetch with zero options (so it
* will never get a value). Lets a dependent panel fall through to "no data"
@@ -112,7 +106,6 @@ export const createVariableFetchSlice: StateCreator<
variableFetchStates: {},
variableLastUpdated: {},
variableCycleIds: {},
variableCycleReasons: {},
variableResolvedEmpty: {},
variableFetchContext: null,
lastFetchAllKey: null,
@@ -122,7 +115,6 @@ export const createVariableFetchSlice: StateCreator<
variableFetchStates: {},
variableLastUpdated: {},
variableCycleIds: {},
variableCycleReasons: {},
variableResolvedEmpty: {},
variableFetchContext: null,
lastFetchAllKey: null,
@@ -140,7 +132,6 @@ export const createVariableFetchSlice: StateCreator<
initVariableFetch: (names, context): void => {
const maps = cloneMaps(get());
const resolvedEmpty = { ...get().variableResolvedEmpty };
const reasons = { ...get().variableCycleReasons };
names.forEach((name) => {
if (!maps.states[name]) {
maps.states[name] = VariableFetchState.Idle;
@@ -153,14 +144,12 @@ export const createVariableFetchSlice: StateCreator<
delete maps.lastUpdated[name];
delete maps.cycleIds[name];
delete resolvedEmpty[name];
delete reasons[name];
}
});
set({
variableFetchStates: maps.states,
variableLastUpdated: maps.lastUpdated,
variableCycleIds: maps.cycleIds,
variableCycleReasons: reasons,
variableResolvedEmpty: resolvedEmpty,
variableFetchContext: context,
});
@@ -182,11 +171,6 @@ export const createVariableFetchSlice: StateCreator<
dynamicVariableOrder,
} = variableFetchContext;
const maps = cloneMaps(get());
const reasons = { ...get().variableCycleReasons };
const bump = (name: string): void => {
maps.cycleIds[name] = (maps.cycleIds[name] || 0) + 1;
reasons[name] = VariableCycleReason.FullCycle;
};
// Query variables wait only for their QUERY parents. A DYNAMIC parent does not
// gate: its option fetch feeds only its own dropdown, while its selected value
@@ -194,7 +178,7 @@ export const createVariableFetchSlice: StateCreator<
// dependent query substitutes it immediately and refetches via the cascade if
// it later changes. Text/custom parents resolve synchronously, so nothing waits.
queryVariableOrder.forEach((name) => {
bump(name);
maps.cycleIds[name] = (maps.cycleIds[name] || 0) + 1;
const parents = dependencyData.parentGraph[name] || [];
const hasQueryParents = parents.some((p) => variableTypes[p] === 'QUERY');
maps.states[name] = hasQueryParents
@@ -208,7 +192,7 @@ export const createVariableFetchSlice: StateCreator<
const orderedQuery = new Set(queryVariableOrder);
Object.keys(variableTypes).forEach((name) => {
if (variableTypes[name] === 'QUERY' && !orderedQuery.has(name)) {
bump(name);
maps.cycleIds[name] = (maps.cycleIds[name] || 0) + 1;
maps.states[name] = resolveFetchState(maps, name);
}
});
@@ -219,7 +203,7 @@ export const createVariableFetchSlice: StateCreator<
// populate fast even when query variables are slow; a sibling selection change
// later refetches them via `enqueueDescendantsBatch`.
dynamicVariableOrder.forEach((name) => {
bump(name);
maps.cycleIds[name] = (maps.cycleIds[name] || 0) + 1;
maps.states[name] = resolveFetchState(maps, name);
});
@@ -227,7 +211,6 @@ export const createVariableFetchSlice: StateCreator<
variableFetchStates: maps.states,
variableLastUpdated: maps.lastUpdated,
variableCycleIds: maps.cycleIds,
variableCycleReasons: reasons,
lastFetchAllKey: key ?? get().lastFetchAllKey,
});
},
@@ -307,11 +290,6 @@ export const createVariableFetchSlice: StateCreator<
const { dependencyData, variableTypes, dynamicVariableOrder } =
variableFetchContext;
const maps = cloneMaps(get());
const reasons = { ...get().variableCycleReasons };
const bump = (name: string): void => {
maps.cycleIds[name] = (maps.cycleIds[name] || 0) + 1;
reasons[name] = VariableCycleReason.ValueCascade;
};
const changed = new Set(names);
// Callers commit values before this runs, so the gate sees the new parent values.
const selection = selectVariableValues(get().dashboardId)(get());
@@ -327,7 +305,7 @@ export const createVariableFetchSlice: StateCreator<
});
});
queryDescendants.forEach((desc) => {
bump(desc);
maps.cycleIds[desc] = (maps.cycleIds[desc] || 0) + 1;
maps.states[desc] = queryParentsHaveValues(
desc,
variableFetchContext,
@@ -344,7 +322,7 @@ export const createVariableFetchSlice: StateCreator<
dynamicVariableOrder
.filter((dynName) => !changed.has(dynName))
.forEach((dynName) => {
bump(dynName);
maps.cycleIds[dynName] = (maps.cycleIds[dynName] || 0) + 1;
maps.states[dynName] = resolveFetchState(maps, dynName);
});
}
@@ -353,7 +331,6 @@ export const createVariableFetchSlice: StateCreator<
variableFetchStates: maps.states,
variableLastUpdated: maps.lastUpdated,
variableCycleIds: maps.cycleIds,
variableCycleReasons: reasons,
});
},
});
@@ -370,12 +347,6 @@ export const selectVariableCycleId =
(state: DashboardStore): number =>
state.variableCycleIds[name] ?? 0;
/** Selector: why a variable's cycle was enqueued. Undefined for types that never fetch. */
export const selectVariableCycleReason =
(name: string) =>
(state: DashboardStore): VariableCycleReason | undefined =>
state.variableCycleReasons[name];
/** Selector: whether a variable has completed at least one fetch. */
export const selectVariableFetchedOnce =
(name: string) =>

View File

@@ -7,14 +7,6 @@ export enum VariableFetchState {
Error = 'error',
}
/** Why a cycle was started — only a cascade may re-default a user's selection. */
export enum VariableCycleReason {
/** `enqueueFetchAll`: load, time-range or variable-order change. */
FullCycle = 'full-cycle',
/** `enqueueDescendantsBatch`: a parent or sibling variable's value changed. */
ValueCascade = 'value-cascade',
}
/** Mutable clones a fetch action works over before committing back in one `set`. */
export interface FetchMaps {
states: Record<string, VariableFetchState>;

View File

@@ -4,6 +4,8 @@ import { CardContainer } from 'container/GridCardLayout/styles';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { Widgets } from 'types/api/dashboard/getAll';
import { FeatureKeys } from '../../../../constants/features';
import { useAppContext } from '../../../../providers/App/App';
import MetricPageGridGraph from './MetricPageGraph';
import {
getAverageRequestLatencyWidgetData,
@@ -71,15 +73,20 @@ function MetricColumnGraphs({
}): JSX.Element {
const { t } = useTranslation('messagingQueues');
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const metricsData = [
{
title: t('metricGraphCategory.brokerMetrics.title'),
description: t('metricGraphCategory.brokerMetrics.description'),
graphCount: [
getBrokerCountWidgetData(),
getRequestTimesWidgetData(),
getProducerFetchRequestPurgatoryWidgetData(),
getBrokerNetworkThroughputWidgetData(),
getBrokerCountWidgetData(dotMetricsEnabled),
getRequestTimesWidgetData(dotMetricsEnabled),
getProducerFetchRequestPurgatoryWidgetData(dotMetricsEnabled),
getBrokerNetworkThroughputWidgetData(dotMetricsEnabled),
],
id: 'broker-metrics',
},
@@ -87,11 +94,11 @@ function MetricColumnGraphs({
title: t('metricGraphCategory.producerMetrics.title'),
description: t('metricGraphCategory.producerMetrics.description'),
graphCount: [
getIoWaitTimeWidgetData(),
getRequestResponseWidgetData(),
getAverageRequestLatencyWidgetData(),
getKafkaProducerByteRateWidgetData(),
getBytesConsumedWidgetData(),
getIoWaitTimeWidgetData(dotMetricsEnabled),
getRequestResponseWidgetData(dotMetricsEnabled),
getAverageRequestLatencyWidgetData(dotMetricsEnabled),
getKafkaProducerByteRateWidgetData(dotMetricsEnabled),
getBytesConsumedWidgetData(dotMetricsEnabled),
],
id: 'producer-metrics',
},
@@ -99,11 +106,11 @@ function MetricColumnGraphs({
title: t('metricGraphCategory.consumerMetrics.title'),
description: t('metricGraphCategory.consumerMetrics.description'),
graphCount: [
getConsumerOffsetWidgetData(),
getConsumerGroupMemberWidgetData(),
getConsumerLagByGroupWidgetData(),
getConsumerFetchRateWidgetData(),
getMessagesConsumedWidgetData(),
getConsumerOffsetWidgetData(dotMetricsEnabled),
getConsumerGroupMemberWidgetData(dotMetricsEnabled),
getConsumerLagByGroupWidgetData(dotMetricsEnabled),
getConsumerFetchRateWidgetData(dotMetricsEnabled),
getMessagesConsumedWidgetData(dotMetricsEnabled),
],
id: 'consumer-metrics',
},

View File

@@ -8,6 +8,8 @@ import { useIsDarkMode } from 'hooks/useDarkMode';
import { ChevronDown, ChevronUp } from '@signozhq/icons';
import { Widgets } from 'types/api/dashboard/getAll';
import { FeatureKeys } from '../../../../constants/features';
import { useAppContext } from '../../../../providers/App/App';
import MetricColumnGraphs from './MetricColumnGraphs';
import MetricPageGridGraph from './MetricPageGraph';
import {
@@ -95,6 +97,11 @@ function MetricPage(): JSX.Element {
}));
};
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const { t } = useTranslation('messagingQueues');
const metricSections = [
@@ -103,10 +110,10 @@ function MetricPage(): JSX.Element {
title: t('metricGraphCategory.brokerJVMMetrics.title'),
description: t('metricGraphCategory.brokerJVMMetrics.description'),
graphCount: [
getJvmGCCountWidgetData(),
getJvmGcCollectionsElapsedWidgetData(),
getCpuRecentUtilizationWidgetData(),
getJvmMemoryHeapWidgetData(),
getJvmGCCountWidgetData(dotMetricsEnabled),
getJvmGcCollectionsElapsedWidgetData(dotMetricsEnabled),
getCpuRecentUtilizationWidgetData(dotMetricsEnabled),
getJvmMemoryHeapWidgetData(dotMetricsEnabled),
],
},
{
@@ -114,10 +121,10 @@ function MetricPage(): JSX.Element {
title: t('metricGraphCategory.partitionMetrics.title'),
description: t('metricGraphCategory.partitionMetrics.description'),
graphCount: [
getPartitionCountPerTopicWidgetData(),
getCurrentOffsetPartitionWidgetData(),
getOldestOffsetWidgetData(),
getInsyncReplicasWidgetData(),
getPartitionCountPerTopicWidgetData(dotMetricsEnabled),
getCurrentOffsetPartitionWidgetData(dotMetricsEnabled),
getOldestOffsetWidgetData(dotMetricsEnabled),
getInsyncReplicasWidgetData(dotMetricsEnabled),
],
},
];
@@ -131,7 +138,7 @@ function MetricPage(): JSX.Element {
// Only log when first graph has rendered and we haven't logged yet
if (renderedGraphCountRef.current === 1 && !hasLoggedRef.current) {
void logEvent('MQ Kafka: Metric view', {
logEvent('MQ Kafka: Metric view', {
graphRendered: true,
});
hasLoggedRef.current = true;

View File

@@ -78,15 +78,21 @@ export function getWidgetQuery(
};
}
export const getRequestTimesWidgetData = (): Widgets =>
export const getRequestTimesWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.request.time.avg',
id: 'kafka.request.time.avg--float64--Gauge--true',
// choose key based on flag
key: dotMetricsEnabled
? 'kafka.request.time.avg'
: 'kafka_request_time_avg',
// mirror into the id as well
id: 'kafka_request_time_avg--float64--Gauge--true',
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -116,15 +122,15 @@ export const getRequestTimesWidgetData = (): Widgets =>
}),
);
export const getBrokerCountWidgetData = (): Widgets =>
export const getBrokerCountWidgetData = (dotMetricsEnabled: boolean): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.brokers',
id: 'kafka.brokers--float64--Gauge--true',
key: dotMetricsEnabled ? 'kafka.brokers' : 'kafka_brokers',
id: 'kafka_brokers--float64--Gauge--true',
type: 'Gauge',
},
aggregateOperator: 'sum',
@@ -150,15 +156,20 @@ export const getBrokerCountWidgetData = (): Widgets =>
}),
);
export const getProducerFetchRequestPurgatoryWidgetData = (): Widgets =>
export const getProducerFetchRequestPurgatoryWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.purgatory.size',
id: 'kafka.purgatory.size--float64--Gauge--true',
// inline ternary based on dotMetricsEnabled
key: dotMetricsEnabled ? 'kafka.purgatory.size' : 'kafka_purgatory_size',
id: `${
dotMetricsEnabled ? 'kafka.purgatory.size' : 'kafka_purgatory_size'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -185,15 +196,24 @@ export const getProducerFetchRequestPurgatoryWidgetData = (): Widgets =>
}),
);
export const getBrokerNetworkThroughputWidgetData = (): Widgets =>
export const getBrokerNetworkThroughputWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka_server_brokertopicmetrics_total_replicationbytesinpersec_oneminuterate',
id: 'kafka_server_brokertopicmetrics_total_replicationbytesinpersec_oneminuterate--float64--Gauge--true',
// inline ternary based on dotMetricsEnabled
key: dotMetricsEnabled
? 'kafka_server_brokertopicmetrics_total_replicationbytesinpersec_oneminuterate'
: 'kafka_server_brokertopicmetrics_bytesoutpersec_oneminuterate',
id: `${
dotMetricsEnabled
? 'kafka_server_brokertopicmetrics_total_replicationbytesinpersec_oneminuterate'
: 'kafka_server_brokertopicmetrics_bytesoutpersec_oneminuterate'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -220,15 +240,22 @@ export const getBrokerNetworkThroughputWidgetData = (): Widgets =>
}),
);
export const getIoWaitTimeWidgetData = (): Widgets =>
export const getIoWaitTimeWidgetData = (dotMetricsEnabled: boolean): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.producer.io_waittime_total',
id: 'kafka.producer.io_waittime_total--float64--Sum--true',
// inline ternary based on dotMetricsEnabled
key: dotMetricsEnabled
? 'kafka.producer.io_waittime_total'
: 'kafka_producer_io_waittime_total',
id: `${
dotMetricsEnabled
? 'kafka.producer.io_waittime_total'
: 'kafka_producer_io_waittime_total'
}--float64--Sum--true`,
type: 'Sum',
},
aggregateOperator: 'rate',
@@ -255,15 +282,23 @@ export const getIoWaitTimeWidgetData = (): Widgets =>
}),
);
export const getRequestResponseWidgetData = (): Widgets =>
export const getRequestResponseWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.producer.request_rate',
id: 'kafka.producer.request_rate--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.producer.request_rate'
: 'kafka_producer_request_rate',
id: `${
dotMetricsEnabled
? 'kafka.producer.request_rate'
: 'kafka_producer_request_rate'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -286,8 +321,14 @@ export const getRequestResponseWidgetData = (): Widgets =>
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.producer.response_rate',
id: 'kafka.producer.response_rate--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.producer.response_rate'
: 'kafka_producer_response_rate',
id: `${
dotMetricsEnabled
? 'kafka.producer.response_rate'
: 'kafka_producer_response_rate'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -314,15 +355,23 @@ export const getRequestResponseWidgetData = (): Widgets =>
}),
);
export const getAverageRequestLatencyWidgetData = (): Widgets =>
export const getAverageRequestLatencyWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.producer.request_latency_avg',
id: 'kafka.producer.request_latency_avg--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.producer.request_latency_avg'
: 'kafka_producer_request_latency_avg',
id: `${
dotMetricsEnabled
? 'kafka.producer.request_latency_avg'
: 'kafka_producer_request_latency_avg'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -349,15 +398,23 @@ export const getAverageRequestLatencyWidgetData = (): Widgets =>
}),
);
export const getKafkaProducerByteRateWidgetData = (): Widgets =>
export const getKafkaProducerByteRateWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.producer.byte_rate',
id: 'kafka.producer.byte_rate--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.producer.byte_rate'
: 'kafka_producer_byte_rate',
id: `${
dotMetricsEnabled
? 'kafka.producer.byte_rate'
: 'kafka_producer_byte_rate'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -385,21 +442,31 @@ export const getKafkaProducerByteRateWidgetData = (): Widgets =>
timeAggregation: 'avg',
},
],
title: 'kafka.producer.byte_rate',
title: dotMetricsEnabled
? 'kafka.producer.byte_rate'
: 'kafka_producer_byte_rate',
description:
'Helps measure the data output rate from the producer, indicating the load a producer is placing on Kafka brokers.',
}),
);
export const getBytesConsumedWidgetData = (): Widgets =>
export const getBytesConsumedWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.consumer.bytes_consumed_rate',
id: 'kafka.consumer.bytes_consumed_rate--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.consumer.bytes_consumed_rate'
: 'kafka_consumer_bytes_consumed_rate',
id: `${
dotMetricsEnabled
? 'kafka.consumer.bytes_consumed_rate'
: 'kafka_consumer_bytes_consumed_rate'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -427,15 +494,23 @@ export const getBytesConsumedWidgetData = (): Widgets =>
}),
);
export const getConsumerOffsetWidgetData = (): Widgets =>
export const getConsumerOffsetWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.consumer_group.offset',
id: 'kafka.consumer_group.offset--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.consumer_group.offset'
: 'kafka_consumer_group_offset',
id: `${
dotMetricsEnabled
? 'kafka.consumer_group.offset'
: 'kafka_consumer_group_offset'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -481,15 +556,23 @@ export const getConsumerOffsetWidgetData = (): Widgets =>
}),
);
export const getConsumerGroupMemberWidgetData = (): Widgets =>
export const getConsumerGroupMemberWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.consumer_group.members',
id: 'kafka.consumer_group.members--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.consumer_group.members'
: 'kafka_consumer_group_members',
id: `${
dotMetricsEnabled
? 'kafka.consumer_group.members'
: 'kafka_consumer_group_members'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'sum',
@@ -522,15 +605,23 @@ export const getConsumerGroupMemberWidgetData = (): Widgets =>
}),
);
export const getConsumerLagByGroupWidgetData = (): Widgets =>
export const getConsumerLagByGroupWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.consumer_group.lag',
id: 'kafka.consumer_group.lag--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.consumer_group.lag'
: 'kafka_consumer_group_lag',
id: `${
dotMetricsEnabled
? 'kafka.consumer_group.lag'
: 'kafka_consumer_group_lag'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -576,15 +667,23 @@ export const getConsumerLagByGroupWidgetData = (): Widgets =>
}),
);
export const getConsumerFetchRateWidgetData = (): Widgets =>
export const getConsumerFetchRateWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.consumer.fetch_rate',
id: 'kafka.consumer.fetch_rate--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.consumer.fetch_rate'
: 'kafka_consumer_fetch_rate',
id: `${
dotMetricsEnabled
? 'kafka.consumer.fetch_rate'
: 'kafka_consumer_fetch_rate'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -597,7 +696,7 @@ export const getConsumerFetchRateWidgetData = (): Widgets =>
{
dataType: DataTypes.String,
id: 'service_name--string--tag--false',
key: 'service.name',
key: dotMetricsEnabled ? 'service.name' : 'service_name',
type: 'tag',
},
],
@@ -618,15 +717,23 @@ export const getConsumerFetchRateWidgetData = (): Widgets =>
}),
);
export const getMessagesConsumedWidgetData = (): Widgets =>
export const getMessagesConsumedWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.consumer.records_consumed_rate',
id: 'kafka.consumer.records_consumed_rate--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.consumer.records_consumed_rate'
: 'kafka_consumer_records_consumed_rate',
id: `${
dotMetricsEnabled
? 'kafka.consumer.records_consumed_rate'
: 'kafka_consumer_records_consumed_rate'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -653,15 +760,21 @@ export const getMessagesConsumedWidgetData = (): Widgets =>
}),
);
export const getJvmGCCountWidgetData = (): Widgets =>
export const getJvmGCCountWidgetData = (dotMetricsEnabled: boolean): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'jvm.gc.collections.count',
id: 'jvm.gc.collections.count--float64--Sum--true',
key: dotMetricsEnabled
? 'jvm.gc.collections.count'
: 'jvm_gc_collections_count',
id: `${
dotMetricsEnabled
? 'jvm.gc.collections.count'
: 'jvm_gc_collections_count'
}--float64--Sum--true`,
type: 'Sum',
},
aggregateOperator: 'rate',
@@ -688,15 +801,23 @@ export const getJvmGCCountWidgetData = (): Widgets =>
}),
);
export const getJvmGcCollectionsElapsedWidgetData = (): Widgets =>
export const getJvmGcCollectionsElapsedWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'jvm.gc.collections.elapsed',
id: 'jvm.gc.collections.elapsed--float64--Sum--true',
key: dotMetricsEnabled
? 'jvm.gc.collections.elapsed'
: 'jvm_gc_collections_elapsed',
id: `${
dotMetricsEnabled
? 'jvm.gc.collections.elapsed'
: 'jvm_gc_collections_elapsed'
}--float64--Sum--true`,
type: 'Sum',
},
aggregateOperator: 'rate',
@@ -717,21 +838,31 @@ export const getJvmGcCollectionsElapsedWidgetData = (): Widgets =>
timeAggregation: 'rate',
},
],
title: 'jvm.gc.collections.elapsed',
title: dotMetricsEnabled
? 'jvm.gc.collections.elapsed'
: 'jvm_gc_collections_elapsed',
description:
'Measures the total time (usually in milliseconds) spent on garbage collection (GC) events in the Java Virtual Machine (JVM).',
}),
);
export const getCpuRecentUtilizationWidgetData = (): Widgets =>
export const getCpuRecentUtilizationWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'jvm.cpu.recent_utilization',
id: 'jvm.cpu.recent_utilization--float64--Gauge--true',
key: dotMetricsEnabled
? 'jvm.cpu.recent_utilization'
: 'jvm_cpu_recent_utilization',
id: `${
dotMetricsEnabled
? 'jvm.cpu.recent_utilization'
: 'jvm_cpu_recent_utilization'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -758,15 +889,19 @@ export const getCpuRecentUtilizationWidgetData = (): Widgets =>
}),
);
export const getJvmMemoryHeapWidgetData = (): Widgets =>
export const getJvmMemoryHeapWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'jvm.memory.heap.max',
id: 'jvm.memory.heap.max--float64--Gauge--true',
key: dotMetricsEnabled ? 'jvm.memory.heap.max' : 'jvm_memory_heap_max',
id: `${
dotMetricsEnabled ? 'jvm.memory.heap.max' : 'jvm_memory_heap_max'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -793,15 +928,21 @@ export const getJvmMemoryHeapWidgetData = (): Widgets =>
}),
);
export const getPartitionCountPerTopicWidgetData = (): Widgets =>
export const getPartitionCountPerTopicWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.topic.partitions',
id: 'kafka.topic.partitions--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.topic.partitions'
: 'kafka_topic_partitions',
id: `${
dotMetricsEnabled ? 'kafka.topic.partitions' : 'kafka_topic_partitions'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'sum',
@@ -834,15 +975,23 @@ export const getPartitionCountPerTopicWidgetData = (): Widgets =>
}),
);
export const getCurrentOffsetPartitionWidgetData = (): Widgets =>
export const getCurrentOffsetPartitionWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.partition.current_offset',
id: 'kafka.partition.current_offset--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.partition.current_offset'
: 'kafka_partition_current_offset',
id: `${
dotMetricsEnabled
? 'kafka.partition.current_offset'
: 'kafka_partition_current_offset'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -882,15 +1031,23 @@ export const getCurrentOffsetPartitionWidgetData = (): Widgets =>
}),
);
export const getOldestOffsetWidgetData = (): Widgets =>
export const getOldestOffsetWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.partition.oldest_offset',
id: 'kafka.partition.oldest_offset--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.partition.oldest_offset'
: 'kafka_partition_oldest_offset',
id: `${
dotMetricsEnabled
? 'kafka.partition.oldest_offset'
: 'kafka_partition_oldest_offset'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',
@@ -930,15 +1087,23 @@ export const getOldestOffsetWidgetData = (): Widgets =>
}),
);
export const getInsyncReplicasWidgetData = (): Widgets =>
export const getInsyncReplicasWidgetData = (
dotMetricsEnabled: boolean,
): Widgets =>
getWidgetQueryBuilder(
getWidgetQuery({
queryData: [
{
aggregateAttribute: {
dataType: DataTypes.Float64,
key: 'kafka.partition.replicas_in_sync',
id: 'kafka.partition.replicas_in_sync--float64--Gauge--true',
key: dotMetricsEnabled
? 'kafka.partition.replicas_in_sync'
: 'kafka_partition_replicas_in_sync',
id: `${
dotMetricsEnabled
? 'kafka.partition.replicas_in_sync'
: 'kafka_partition_replicas_in_sync'
}--float64--Gauge--true`,
type: 'Gauge',
},
aggregateOperator: 'avg',

View File

@@ -11,6 +11,8 @@ import useDebouncedFn from 'hooks/useDebouncedFunction';
import useUrlQuery from 'hooks/useUrlQuery';
import { Check, Share2 } from '@signozhq/icons';
import { FeatureKeys } from '../../../constants/features';
import { useAppContext } from '../../../providers/App/App';
import { useGetAllConfigOptions } from './useGetAllConfigOptions';
import './MQConfigOptions.styles.scss';
@@ -38,11 +40,19 @@ const useConfigOptions = (
isFetching: boolean;
options: DefaultOptionType[];
} => {
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const [searchText, setSearchText] = useState<string>('');
const { isFetching, options } = useGetAllConfigOptions({
attributeKey: type,
searchText,
});
const { isFetching, options } = useGetAllConfigOptions(
{
attributeKey: type,
searchText,
},
dotMetricsEnabled,
);
const handleDebouncedSearch = useDebouncedFn((searchText): void => {
setSearchText(searchText as string);
}, 500);

View File

@@ -3,6 +3,7 @@ import { useCallback, useMemo, useRef } from 'react';
import { useDispatch } from 'react-redux';
import { useHistory, useLocation } from 'react-router-dom';
import logEvent from 'api/common/logEvent';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ViewMenuAction } from 'container/GridCardLayout/config';
@@ -11,6 +12,7 @@ import { Card } from 'container/GridCardLayout/styles';
import { getWidgetQueryBuilder } from 'container/MetricsApplication/MetricsApplication.factory';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useUrlQuery from 'hooks/useUrlQuery';
import { useAppContext } from 'providers/App/App';
import { UpdateTimeInterval } from 'store/actions';
import {
@@ -32,9 +34,15 @@ function MessagingQueuesGraph(): JSX.Element {
[consumerGrp, topic, partition],
);
const { featureFlags } = useAppContext();
const dotMetricsEnabled =
featureFlags?.find((flag) => flag.name === FeatureKeys.DOT_METRICS_ENABLED)
?.active || false;
const widgetData = useMemo(
() => getWidgetQueryBuilder(getWidgetQuery({ filterItems })),
[filterItems],
() =>
getWidgetQueryBuilder(getWidgetQuery({ filterItems, dotMetricsEnabled })),
[filterItems, dotMetricsEnabled],
);
const history = useHistory();
@@ -73,7 +81,7 @@ function MessagingQueuesGraph(): JSX.Element {
const checkIfDataExists = (isDataAvailable: boolean): void => {
if (!isLogEventCalled.current) {
isLogEventCalled.current = true;
void logEvent('Messaging Queues: Graph data fetched', {
logEvent('Messaging Queues: Graph data fetched', {
isDataAvailable,
});
}

View File

@@ -16,6 +16,7 @@ export interface GetAllConfigOptionsResponse {
export function useGetAllConfigOptions(
props: ConfigOptions,
dotMetricsEnabled: boolean,
): GetAllConfigOptionsResponse {
const { attributeKey, searchText } = props;
@@ -25,7 +26,9 @@ export function useGetAllConfigOptions(
const { payload } = await getAttributesValues({
aggregateOperator: 'avg',
dataSource: DataSource.METRICS,
aggregateAttribute: 'kafka.consumer_group.lag',
aggregateAttribute: dotMetricsEnabled
? 'kafka.consumer_group.lag'
: 'kafka_consumer_group_lag',
attributeKey,
searchText: searchText ?? '',
filterAttributeKeyDataType: DataTypes.String,

View File

@@ -94,8 +94,10 @@ export function getFiltersFromConfigOptions(
export function getWidgetQuery({
filterItems,
dotMetricsEnabled,
}: {
filterItems: TagFilterItem[];
dotMetricsEnabled: boolean;
}): GetWidgetQueryBuilderProps {
return {
title: 'Consumer Lag',
@@ -110,8 +112,14 @@ export function getWidgetQuery({
{
aggregateAttribute: {
dataType: DataTypes.Float64,
id: 'kafka.consumer_group.lag--float64--Gauge--true',
key: 'kafka.consumer_group.lag',
id: `${
dotMetricsEnabled
? 'kafka.consumer_group.lag'
: 'kafka_consumer_group_lag'
}--float64--Gauge--true`,
key: dotMetricsEnabled
? 'kafka.consumer_group.lag'
: 'kafka_consumer_group_lag',
type: 'Gauge',
},
aggregateOperator: 'max',

View File

@@ -0,0 +1,9 @@
export interface Props {
token: string;
password: string;
}
export interface PayloadProps {
data: string;
status: string;
}

View File

@@ -0,0 +1,14 @@
import { User } from 'types/reducer/app';
import { ROLES } from 'types/roles';
export interface Props {
name: User['displayName'];
email: User['email'];
role: ROLES;
frontendBaseUrl: string;
}
export interface PayloadProps {
data: string;
status: string;
}

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