mirror of
https://github.com/SigNoz/signoz.git
synced 2026-08-23 05:00:30 +01:00
Compare commits
1 Commits
feat/manua
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9997c3da9c |
@@ -62,6 +62,40 @@ if (typeof window.ResizeObserver === 'undefined') {
|
||||
(window as any).ResizeObserver = ResizeObserverMock;
|
||||
}
|
||||
|
||||
if (typeof globalThis.DOMRect === 'undefined') {
|
||||
(globalThis as any).DOMRect = class DOMRect {
|
||||
x = 0;
|
||||
y = 0;
|
||||
width = 0;
|
||||
height = 0;
|
||||
top = 0;
|
||||
right = 0;
|
||||
bottom = 0;
|
||||
left = 0;
|
||||
constructor(x = 0, y = 0, width = 0, height = 0) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.top = y;
|
||||
this.right = x + width;
|
||||
this.bottom = y + height;
|
||||
this.left = x;
|
||||
}
|
||||
toJSON(): any {
|
||||
return { x: this.x, y: this.y, width: this.width, height: this.height };
|
||||
}
|
||||
static fromRect(rect?: {
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}): DOMRect {
|
||||
return new DOMRect(rect?.x, rect?.y, rect?.width, rect?.height);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Patch getComputedStyle to handle CSS parsing errors from @signozhq/* packages.
|
||||
// These packages inject CSS at import time via style-inject / vite-plugin-css-injected-by-js.
|
||||
// jsdom's nwsapi cannot parse some of the injected selectors (e.g. Tailwind's :animate-in),
|
||||
|
||||
@@ -48,9 +48,9 @@
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@sentry/react": "10.57.0",
|
||||
"@sentry/vite-plugin": "5.3.0",
|
||||
"@signozhq/design-tokens": "2.1.4",
|
||||
"@signozhq/design-tokens": "2.1.6",
|
||||
"@signozhq/icons": "0.4.0",
|
||||
"@signozhq/ui": "0.0.23",
|
||||
"@signozhq/ui": "0.1.0",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "3.13.22",
|
||||
"@uiw/codemirror-theme-copilot": "4.23.11",
|
||||
@@ -238,4 +238,4 @@
|
||||
"tmp@<0.2.6": ">=0.2.6 <0.3.0",
|
||||
"yaml@>=1.0.0 <1.10.3": ">=1.10.3 <2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
823
frontend/pnpm-lock.yaml
generated
823
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -28,9 +28,6 @@ interface FieldsSelectorProps {
|
||||
signal: DataSource;
|
||||
maxFields?: number;
|
||||
requiredFields?: readonly string[];
|
||||
// Lets users add a free-typed field which
|
||||
// does not show up in the suggestions
|
||||
allowCustomFields?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
defaultPosition?: { x: number; y: number };
|
||||
@@ -49,7 +46,6 @@ function FieldsSelectorContent({
|
||||
signal,
|
||||
maxFields,
|
||||
requiredFields,
|
||||
allowCustomFields,
|
||||
width = DEFAULT_PANEL_WIDTH,
|
||||
height,
|
||||
defaultPosition,
|
||||
@@ -71,7 +67,7 @@ function FieldsSelectorContent({
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
const value = e.target.value.trim();
|
||||
const value = e.target.value.trim().toLowerCase();
|
||||
setInputValue(value);
|
||||
debouncedUpdate(value);
|
||||
},
|
||||
@@ -157,7 +153,6 @@ function FieldsSelectorContent({
|
||||
addedFields={draftFields}
|
||||
onAdd={handleAdd}
|
||||
isAtLimit={isAtLimit}
|
||||
allowCustomFields={allowCustomFields}
|
||||
/>
|
||||
|
||||
{hasUnsavedChanges && (
|
||||
@@ -197,7 +192,7 @@ function FieldsSelector({
|
||||
() =>
|
||||
fields.map((f) => ({
|
||||
...f,
|
||||
key: buildCompositeKey(f.name, f.fieldContext),
|
||||
key: f.key ?? buildCompositeKey(f.name, f.fieldContext),
|
||||
})),
|
||||
[fields],
|
||||
);
|
||||
|
||||
@@ -21,7 +21,6 @@ interface OtherFieldsProps {
|
||||
addedFields: TelemetryFieldKey[];
|
||||
onAdd: (field: TelemetryFieldKey) => void;
|
||||
isAtLimit: boolean;
|
||||
allowCustomFields?: boolean;
|
||||
}
|
||||
|
||||
function OtherFields({
|
||||
@@ -30,7 +29,6 @@ function OtherFields({
|
||||
addedFields,
|
||||
onAdd,
|
||||
isAtLimit,
|
||||
allowCustomFields,
|
||||
}: OtherFieldsProps): JSX.Element {
|
||||
const { data, isFetching } = useGetQueryKeySuggestions(
|
||||
{
|
||||
@@ -47,45 +45,25 @@ function OtherFields({
|
||||
},
|
||||
);
|
||||
|
||||
const otherFields = useMemo<TelemetryFieldKey[]>(() => {
|
||||
const rawSuggestions = Object.values(data?.data.data.keys || {}).flat();
|
||||
const otherFields: TelemetryFieldKey[] = useMemo(() => {
|
||||
const suggestions = Object.values(data?.data.data.keys || {}).flat();
|
||||
// Normalize: synthesize `key` once so downstream reads can trust it.
|
||||
const suggestions: TelemetryFieldKey[] = rawSuggestions.map((attr) => ({
|
||||
...attr,
|
||||
key: buildCompositeKey(attr.name, attr.fieldContext as string),
|
||||
signal: attr.signal as SignalType,
|
||||
fieldContext: attr.fieldContext as FieldContext,
|
||||
fieldDataType: attr.fieldDataType,
|
||||
}));
|
||||
const addedIds = new Set(
|
||||
addedFields.map((f) => buildCompositeKey(f.name, f.fieldContext)),
|
||||
const normalizedSuggestions: TelemetryFieldKey[] = suggestions.map(
|
||||
(attr) => ({
|
||||
...attr,
|
||||
key: buildCompositeKey(attr.name, attr.fieldContext as string),
|
||||
signal: attr.signal as SignalType,
|
||||
fieldContext: attr.fieldContext as FieldContext,
|
||||
fieldDataType: attr.fieldDataType,
|
||||
}),
|
||||
);
|
||||
const available = suggestions.filter(
|
||||
const addedIds = new Set(
|
||||
addedFields.map((f) => f.key ?? buildCompositeKey(f.name, f.fieldContext)),
|
||||
);
|
||||
return normalizedSuggestions.filter(
|
||||
(attr) => !addedIds.has(attr.key as string),
|
||||
);
|
||||
|
||||
// Prepend the custom field when its name is not in suggestions and
|
||||
// not already added.
|
||||
const typed = debouncedInputValue.trim();
|
||||
const nameMatches = (list: TelemetryFieldKey[]): boolean =>
|
||||
list.some((f) => f.name.toLowerCase() === typed.toLowerCase());
|
||||
const showCustom =
|
||||
!!allowCustomFields &&
|
||||
typed.length > 0 &&
|
||||
!nameMatches(suggestions) &&
|
||||
!nameMatches(addedFields);
|
||||
|
||||
if (!showCustom) {
|
||||
return available;
|
||||
}
|
||||
const customField: TelemetryFieldKey = {
|
||||
name: typed,
|
||||
fieldContext: '',
|
||||
fieldDataType: '',
|
||||
key: buildCompositeKey(typed, ''),
|
||||
};
|
||||
return [customField, ...available];
|
||||
}, [data, addedFields, allowCustomFields, debouncedInputValue]);
|
||||
}, [data, addedFields]);
|
||||
|
||||
if (isFetching) {
|
||||
return (
|
||||
|
||||
@@ -11,7 +11,7 @@ const makeField = (name: string, fieldContext = 'log'): TelemetryFieldKey => ({
|
||||
signal: 'logs',
|
||||
fieldContext: fieldContext as TelemetryFieldKey['fieldContext'],
|
||||
fieldDataType: 'string',
|
||||
key: `${fieldContext}:${name}`,
|
||||
key: `${fieldContext}.${name}`,
|
||||
});
|
||||
|
||||
describe('AddedFields — requiredFields', () => {
|
||||
@@ -33,7 +33,7 @@ describe('AddedFields — requiredFields', () => {
|
||||
inputValue=""
|
||||
fields={fields}
|
||||
onFieldsChange={jest.fn()}
|
||||
requiredFields={['log:a', 'log:c']}
|
||||
requiredFields={['log.a', 'log.c']}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('AddedFields — requiredFields', () => {
|
||||
inputValue=""
|
||||
fields={fields}
|
||||
onFieldsChange={jest.fn()}
|
||||
requiredFields={['log:a']}
|
||||
requiredFields={['log.a']}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('AddedFields — requiredFields', () => {
|
||||
inputValue=""
|
||||
fields={fields}
|
||||
onFieldsChange={jest.fn()}
|
||||
requiredFields={['log:body']}
|
||||
requiredFields={['log.body']}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -101,11 +101,11 @@ describe('AddedFields — requiredFields', () => {
|
||||
inputValue=""
|
||||
fields={fields}
|
||||
onFieldsChange={jest.fn()}
|
||||
requiredFields={['log:body']}
|
||||
requiredFields={['log.body']}
|
||||
/>,
|
||||
);
|
||||
|
||||
// 'log:body' locked, 'log:body_extra' removable.
|
||||
// 'log.body' locked, 'log.body_extra' removable.
|
||||
expect(screen.getAllByRole('button', { name: /remove/i })).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
import { act, fireEvent, render, screen } from 'tests/test-utils';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import FieldsSelector from '../FieldsSelector';
|
||||
|
||||
jest.mock('hooks/querySuggestions/useGetQueryKeySuggestions');
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
toast: { success: jest.fn(), error: jest.fn() },
|
||||
}));
|
||||
|
||||
// FloatingPanel is a react-rnd/portal shell — presentation only. Render its
|
||||
// children directly so the test exercises the column-editing behavior.
|
||||
jest.mock('periscope/components/FloatingPanel', () => ({
|
||||
FloatingPanel: ({ children }: { children: React.ReactNode }): JSX.Element => (
|
||||
<div>{children}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
const mockSuggestions = (names: string[]): void => {
|
||||
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
data: {
|
||||
data: {
|
||||
keys: {
|
||||
attributeKeys: names.map((name) => ({
|
||||
name,
|
||||
signal: 'logs',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: '',
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
isFetching: false,
|
||||
});
|
||||
};
|
||||
|
||||
const field = (name: string, fieldContext = 'log'): TelemetryFieldKey => ({
|
||||
name,
|
||||
signal: 'logs',
|
||||
fieldContext: fieldContext as TelemetryFieldKey['fieldContext'],
|
||||
fieldDataType: 'string',
|
||||
});
|
||||
|
||||
const renderPanel = (
|
||||
props: Partial<React.ComponentProps<typeof FieldsSelector>> = {},
|
||||
): { onFieldsChange: jest.Mock } => {
|
||||
const onFieldsChange = jest.fn();
|
||||
render(
|
||||
<FieldsSelector
|
||||
isOpen
|
||||
title="Edit columns"
|
||||
fields={props.fields ?? []}
|
||||
onFieldsChange={onFieldsChange}
|
||||
onClose={jest.fn()}
|
||||
signal={DataSource.LOGS}
|
||||
allowCustomFields
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
return { onFieldsChange };
|
||||
};
|
||||
|
||||
// Type into the search box and flush the 400ms debounce so OtherFields (driven
|
||||
// by the debounced value) recomputes.
|
||||
const typeSearch = (value: string): void => {
|
||||
const input = screen.getByPlaceholderText('Search for a field...');
|
||||
act(() => {
|
||||
fireEvent.change(input, { target: { value } });
|
||||
});
|
||||
act(() => {
|
||||
jest.advanceTimersByTime(400);
|
||||
});
|
||||
};
|
||||
|
||||
describe('FieldsSelector — edit columns (integration)', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
mockSuggestions([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('adds a free-typed field end to end and saves the synthesized key', () => {
|
||||
const { onFieldsChange } = renderPanel({ fields: [field('body')] });
|
||||
|
||||
typeSearch('orderId');
|
||||
|
||||
// custom option surfaces in OTHER FIELDS (only Add button, no suggestions)
|
||||
expect(screen.getByText('orderId')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /^add$/i }));
|
||||
});
|
||||
|
||||
// moved into ADDED FIELDS → OTHER FIELDS has nothing left to offer
|
||||
expect(screen.getByText('No values found')).toBeInTheDocument();
|
||||
|
||||
// Save commits the draft
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /save changes/i }));
|
||||
});
|
||||
|
||||
expect(onFieldsChange).toHaveBeenCalledTimes(1);
|
||||
const saved = onFieldsChange.mock.calls[0][0] as TelemetryFieldKey[];
|
||||
expect(saved).toStrictEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'orderId',
|
||||
fieldContext: '',
|
||||
fieldDataType: '',
|
||||
key: 'orderId',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('adds a suggested field: it moves from OTHER FIELDS into ADDED FIELDS', () => {
|
||||
mockSuggestions(['service.name']);
|
||||
const { onFieldsChange } = renderPanel({ fields: [] });
|
||||
|
||||
const addButton = screen.getByRole('button', { name: /^add$/i });
|
||||
act(() => {
|
||||
fireEvent.click(addButton);
|
||||
});
|
||||
|
||||
// now removable in ADDED FIELDS, no longer offered in OTHER FIELDS
|
||||
expect(screen.getByRole('button', { name: /remove/i })).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /^add$/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /save changes/i }));
|
||||
});
|
||||
const saved = onFieldsChange.mock.calls[0][0] as TelemetryFieldKey[];
|
||||
expect(saved.map((f) => f.name)).toContain('service.name');
|
||||
});
|
||||
|
||||
it('hides the custom option when the typed name is already added', () => {
|
||||
renderPanel({ fields: [field('orderId')] });
|
||||
|
||||
typeSearch('ORDERID');
|
||||
|
||||
// exact name already added → nothing left to offer in OTHER FIELDS
|
||||
expect(screen.queryByText('ORDERID')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('No values found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not offer a custom option when allowCustomFields is off', () => {
|
||||
renderPanel({ fields: [], allowCustomFields: false });
|
||||
|
||||
typeSearch('unknown.a.b.c');
|
||||
|
||||
// no custom row and nothing addable
|
||||
expect(screen.queryByText('unknown.a.b.c')).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /^add$/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('discards an added field, reverting the draft', () => {
|
||||
const { onFieldsChange } = renderPanel({ fields: [field('body')] });
|
||||
|
||||
typeSearch('orderId');
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /^add$/i }));
|
||||
});
|
||||
|
||||
// clear the search so the added list is not filtered
|
||||
typeSearch('');
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /discard/i }));
|
||||
});
|
||||
|
||||
expect(screen.queryByText('orderId')).not.toBeInTheDocument();
|
||||
expect(onFieldsChange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,125 +0,0 @@
|
||||
import { fireEvent, render, screen } from 'tests/test-utils';
|
||||
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
|
||||
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import OtherFields from '../OtherFields';
|
||||
|
||||
jest.mock('hooks/querySuggestions/useGetQueryKeySuggestions');
|
||||
|
||||
const mockSuggestions = (names: string[]): void => {
|
||||
(useGetQueryKeySuggestions as jest.Mock).mockReturnValue({
|
||||
data: {
|
||||
data: {
|
||||
data: {
|
||||
keys: {
|
||||
attributeKeys: names.map((name) => ({
|
||||
name,
|
||||
signal: 'logs',
|
||||
fieldDataType: 'string',
|
||||
fieldContext: '',
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
isFetching: false,
|
||||
});
|
||||
};
|
||||
|
||||
const renderOtherFields = (
|
||||
props: Partial<React.ComponentProps<typeof OtherFields>> = {},
|
||||
): { onAdd: jest.Mock } => {
|
||||
const onAdd = jest.fn();
|
||||
render(
|
||||
<OtherFields
|
||||
signal={DataSource.LOGS}
|
||||
debouncedInputValue=""
|
||||
addedFields={[]}
|
||||
onAdd={onAdd}
|
||||
isAtLimit={false}
|
||||
allowCustomFields
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
return { onAdd };
|
||||
};
|
||||
|
||||
const addedField = (name: string): TelemetryFieldKey => ({
|
||||
name,
|
||||
signal: 'logs',
|
||||
fieldContext: '',
|
||||
fieldDataType: '',
|
||||
key: name,
|
||||
});
|
||||
|
||||
describe('OtherFields — custom (free-typed) option', () => {
|
||||
beforeEach(() => {
|
||||
mockSuggestions([]);
|
||||
});
|
||||
|
||||
it('shows a custom option for a typed name that is not a suggestion', () => {
|
||||
renderOtherFields({ debouncedInputValue: 'unknown.a.b.c' });
|
||||
|
||||
expect(screen.getByText('unknown.a.b.c')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /add/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('synthesizes the field with raw name, empty context/type, on add', () => {
|
||||
const { onAdd } = renderOtherFields({ debouncedInputValue: 'orderId' });
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /add/i }));
|
||||
|
||||
expect(onAdd).toHaveBeenCalledWith({
|
||||
name: 'orderId',
|
||||
fieldContext: '',
|
||||
fieldDataType: '',
|
||||
key: 'orderId',
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the custom option when an exact suggestion exists (case-insensitive)', () => {
|
||||
mockSuggestions(['orderId']);
|
||||
renderOtherFields({ debouncedInputValue: 'orderid' });
|
||||
|
||||
// the real suggestion shows, the lowercased custom name does not
|
||||
expect(screen.getByText('orderId')).toBeInTheDocument();
|
||||
expect(screen.queryByText('orderid')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides the custom option when the name is already added (case-insensitive)', () => {
|
||||
renderOtherFields({
|
||||
debouncedInputValue: 'ORDERID',
|
||||
addedFields: [addedField('orderId')],
|
||||
});
|
||||
|
||||
expect(screen.queryByText('ORDERID')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('No values found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the custom option when allowCustomFields is off', () => {
|
||||
renderOtherFields({
|
||||
debouncedInputValue: 'unknown.a.b.c',
|
||||
allowCustomFields: false,
|
||||
});
|
||||
|
||||
expect(screen.queryByText('unknown.a.b.c')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('No values found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not show the custom option for an empty input', () => {
|
||||
renderOtherFields({ debouncedInputValue: ' ' });
|
||||
|
||||
expect(screen.getByText('No values found')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the custom option at the field limit but hides its Add button', () => {
|
||||
renderOtherFields({ debouncedInputValue: 'unknown.a.b.c', isAtLimit: true });
|
||||
|
||||
// same as every other row at the limit: name shown, no Add button
|
||||
expect(screen.getByText('unknown.a.b.c')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /add/i }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -51,13 +51,13 @@ describe('useLogsTableColumns — selectColumns-order respected', () => {
|
||||
);
|
||||
|
||||
// body/timestamp appear where the caller placed them, keyed by their
|
||||
// composite IDs ('log:*'); contextless user fields collapse to bare name.
|
||||
// composite IDs ('log.*'); contextless user fields collapse to bare name.
|
||||
expect(result.current.map((c) => c.id)).toStrictEqual([
|
||||
'state-indicator',
|
||||
'service.name',
|
||||
'log:body',
|
||||
'log.body',
|
||||
'request.id',
|
||||
'log:timestamp',
|
||||
'log.timestamp',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -70,14 +70,14 @@ describe('useLogsTableColumns — selectColumns-order respected', () => {
|
||||
);
|
||||
|
||||
const byId = new Map(result.current.map((c) => [c.id, c]));
|
||||
// Attribute variant is its own column, not a duplicate 'log:body'.
|
||||
// Attribute variant is its own column, not a duplicate 'log.body'.
|
||||
expect(result.current.map((c) => c.id)).toStrictEqual([
|
||||
'state-indicator',
|
||||
'log:body',
|
||||
'attribute:body',
|
||||
'log.body',
|
||||
'attribute.body',
|
||||
]);
|
||||
expect(byId.get('log:body')?.enableRemove).toBe(false);
|
||||
expect(byId.get('attribute:body')?.enableRemove).toBe(true);
|
||||
expect(byId.get('log.body')?.enableRemove).toBe(false);
|
||||
expect(byId.get('attribute.body')?.enableRemove).toBe(true);
|
||||
});
|
||||
|
||||
it('applies the same distinct-column treatment to timestamp variants', () => {
|
||||
@@ -91,11 +91,11 @@ describe('useLogsTableColumns — selectColumns-order respected', () => {
|
||||
const byId = new Map(result.current.map((c) => [c.id, c]));
|
||||
expect(result.current.map((c) => c.id)).toStrictEqual([
|
||||
'state-indicator',
|
||||
'log:timestamp',
|
||||
'attribute:timestamp',
|
||||
'log.timestamp',
|
||||
'attribute.timestamp',
|
||||
]);
|
||||
expect(byId.get('log:timestamp')?.enableRemove).toBe(false);
|
||||
expect(byId.get('attribute:timestamp')?.enableRemove).toBe(true);
|
||||
expect(byId.get('log.timestamp')?.enableRemove).toBe(false);
|
||||
expect(byId.get('attribute.timestamp')?.enableRemove).toBe(true);
|
||||
});
|
||||
|
||||
it('skips the synthetic "id" field name', () => {
|
||||
@@ -127,10 +127,10 @@ describe('useLogsTableColumns — selectColumns-order respected', () => {
|
||||
|
||||
const byId = new Map(result.current.map((c) => [c.id, c]));
|
||||
// body + timestamp are locked from the table-X removal pathway.
|
||||
expect(byId.get('log:body')?.canBeHidden).toBe(false);
|
||||
expect(byId.get('log:body')?.enableRemove).toBe(false);
|
||||
expect(byId.get('log:timestamp')?.canBeHidden).toBe(false);
|
||||
expect(byId.get('log:timestamp')?.enableRemove).toBe(false);
|
||||
expect(byId.get('log.body')?.canBeHidden).toBe(false);
|
||||
expect(byId.get('log.body')?.enableRemove).toBe(false);
|
||||
expect(byId.get('log.timestamp')?.canBeHidden).toBe(false);
|
||||
expect(byId.get('log.timestamp')?.enableRemove).toBe(false);
|
||||
// User-added fields stay removable. User field has type='' so composite
|
||||
// collapses to bare name.
|
||||
expect(byId.get('user_field')?.enableRemove).toBe(true);
|
||||
|
||||
@@ -664,6 +664,7 @@ function TanStackTableInner<TData, TItemKey = string>(
|
||||
value={limit?.toString()}
|
||||
defaultValue="10"
|
||||
onChange={(value): void => {
|
||||
value ??= '10';
|
||||
setLimit(+value);
|
||||
pagination.onLimitChange?.(+value);
|
||||
if (page !== 1) {
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
gap: var(--spacing-4);
|
||||
padding: var(--spacing-2) var(--spacing-2);
|
||||
|
||||
--tab-content-padding: 0;
|
||||
--tab-text-color: var(--l1-foreground);
|
||||
--tab-active-text-color: var(--l1-foreground);
|
||||
--tabs-content-padding: 0;
|
||||
--tabs-text-color: var(--l1-foreground);
|
||||
--tabs-active-text-color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.pageError {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
height: 100%;
|
||||
margin-top: var(--spacing-2);
|
||||
margin-left: var(--spacing-2);
|
||||
--tab-text-color: var(--l1-foreground);
|
||||
--tab-active-text-color: var(--l1-foreground);
|
||||
--tabs-text-color: var(--l1-foreground);
|
||||
--tabs-active-text-color: var(--l1-foreground);
|
||||
[role='tabpanel'] {
|
||||
margin: 0;
|
||||
padding: var(--spacing-0) var(--spacing-4);
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-8);
|
||||
--tab-content-padding: 0;
|
||||
--tabs-content-padding: 0;
|
||||
margin-top: var(--spacing-3);
|
||||
--tab-text-color: var(--l1-foreground);
|
||||
--tab-active-text-color: var(--l1-foreground);
|
||||
--tabs-text-color: var(--l1-foreground);
|
||||
--tabs-active-text-color: var(--l1-foreground);
|
||||
}
|
||||
|
||||
.tabLabel {
|
||||
|
||||
@@ -275,7 +275,6 @@ function LiveLogsContainer({
|
||||
onClose={(): void => setIsFieldsSelectorOpen(false)}
|
||||
signal={DataSource.LOGS}
|
||||
requiredFields={LOGS_REQUIRED_COLUMNS}
|
||||
allowCustomFields
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -113,7 +113,6 @@ function LogsActionsContainer({
|
||||
onClose={(): void => setIsFieldsSelectorOpen(false)}
|
||||
signal={DataSource.LOGS}
|
||||
requiredFields={LOGS_REQUIRED_COLUMNS}
|
||||
allowCustomFields
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
}
|
||||
|
||||
// Remove default tab content padding/margin — the card provides spacing.
|
||||
--tab-content-padding: 0;
|
||||
--tab-content-margin: var(--spacing-4) 0 0;
|
||||
--tabs-content-padding: 0;
|
||||
--tabs-content-margin: var(--spacing-4) 0 0;
|
||||
}
|
||||
|
||||
.mcp-client-tabs {
|
||||
|
||||
@@ -296,12 +296,12 @@ describe('useOptionsMenu', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// New order: [attribute:service.name, log:body, resource:service.name, log:timestamp]
|
||||
// New order: [attribute.service.name, log.body, resource.service.name, log.timestamp]
|
||||
result.current.config.addColumn?.onReorder([
|
||||
'attribute:service.name',
|
||||
'log:body',
|
||||
'resource:service.name',
|
||||
'log:timestamp',
|
||||
'attribute.service.name',
|
||||
'log.body',
|
||||
'resource.service.name',
|
||||
'log.timestamp',
|
||||
]);
|
||||
|
||||
expect(mockUpdateColumns).toHaveBeenCalledTimes(1);
|
||||
@@ -309,13 +309,13 @@ describe('useOptionsMenu', () => {
|
||||
expect(
|
||||
reordered.map(
|
||||
(c: { name: string; fieldContext: string }) =>
|
||||
`${c.fieldContext}:${c.name}`,
|
||||
`${c.fieldContext}.${c.name}`,
|
||||
),
|
||||
).toStrictEqual([
|
||||
'attribute:service.name',
|
||||
'log:body',
|
||||
'resource:service.name',
|
||||
'log:timestamp',
|
||||
'attribute.service.name',
|
||||
'log.body',
|
||||
'resource.service.name',
|
||||
'log.timestamp',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -329,11 +329,11 @@ describe('useOptionsMenu', () => {
|
||||
|
||||
result.current.config.addColumn?.onReorder([
|
||||
'state-indicator',
|
||||
'log:timestamp',
|
||||
'log.timestamp',
|
||||
'unknown.composite',
|
||||
'log:body',
|
||||
'resource:service.name',
|
||||
'attribute:service.name',
|
||||
'log.body',
|
||||
'resource.service.name',
|
||||
'attribute.service.name',
|
||||
]);
|
||||
|
||||
const reordered = mockUpdateColumns.mock.calls[0][0];
|
||||
@@ -341,13 +341,13 @@ describe('useOptionsMenu', () => {
|
||||
expect(
|
||||
reordered.map(
|
||||
(c: { name: string; fieldContext: string }) =>
|
||||
`${c.fieldContext}:${c.name}`,
|
||||
`${c.fieldContext}.${c.name}`,
|
||||
),
|
||||
).toStrictEqual([
|
||||
'log:timestamp',
|
||||
'log:body',
|
||||
'resource:service.name',
|
||||
'attribute:service.name',
|
||||
'log.timestamp',
|
||||
'log.body',
|
||||
'resource.service.name',
|
||||
'attribute.service.name',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -359,17 +359,17 @@ describe('useOptionsMenu', () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// Removing 'resource:service.name' should drop ONLY the resource variant.
|
||||
result.current.config.addColumn?.onRemove('resource:service.name');
|
||||
// Removing 'resource.service.name' should drop ONLY the resource variant.
|
||||
result.current.config.addColumn?.onRemove('resource.service.name');
|
||||
|
||||
expect(mockUpdateColumns).toHaveBeenCalledTimes(1);
|
||||
const remaining = mockUpdateColumns.mock.calls[0][0];
|
||||
expect(
|
||||
remaining.map(
|
||||
(c: { name: string; fieldContext: string }) =>
|
||||
`${c.fieldContext}:${c.name}`,
|
||||
`${c.fieldContext}.${c.name}`,
|
||||
),
|
||||
).toStrictEqual(['log:body', 'attribute:service.name', 'log:timestamp']);
|
||||
).toStrictEqual(['log.body', 'attribute.service.name', 'log.timestamp']);
|
||||
});
|
||||
|
||||
it('removing by a non-matching composite ID is a no-op (filter returns the full list)', () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const getOptionsFromKeys = (
|
||||
};
|
||||
|
||||
// Composite identity for a column. Disambiguates same-name fields across
|
||||
// different fieldContexts (e.g. resource:service.name vs attribute:service.name).
|
||||
// different fieldContexts (e.g. resource.service.name vs attribute.service.name).
|
||||
// Falls back to bare name when context is missing.
|
||||
export const buildCompositeKey = (name: string, context?: string): string =>
|
||||
context ? `${context}:${name}` : name;
|
||||
context ? `${context}.${name}` : name;
|
||||
|
||||
@@ -35,7 +35,7 @@ exports[`Value panel wrappper tests should render tooltip when there are conflic
|
||||
class="c0"
|
||||
>
|
||||
<p
|
||||
class="_typography_ulrzs_1"
|
||||
class="_typography_j4pmm_1"
|
||||
data-slot="typography"
|
||||
data-variant="text"
|
||||
/>
|
||||
@@ -50,7 +50,7 @@ exports[`Value panel wrappper tests should render tooltip when there are conflic
|
||||
class="value-text-container"
|
||||
>
|
||||
<p
|
||||
class="_typography_ulrzs_1 value-graph-text"
|
||||
class="_typography_j4pmm_1 value-graph-text"
|
||||
data-slot="typography"
|
||||
data-testid="value-graph-text"
|
||||
data-variant="text"
|
||||
@@ -59,7 +59,7 @@ exports[`Value panel wrappper tests should render tooltip when there are conflic
|
||||
295.43
|
||||
</p>
|
||||
<p
|
||||
class="_typography_ulrzs_1 value-graph-unit"
|
||||
class="_typography_j4pmm_1 value-graph-unit"
|
||||
data-slot="typography"
|
||||
data-testid="value-graph-suffix-unit"
|
||||
data-variant="text"
|
||||
|
||||
@@ -22,11 +22,11 @@ exports[`PipelinePage container test should render DragAction section 1`] = `
|
||||
class="c0"
|
||||
>
|
||||
<div
|
||||
class="_switch-wrapper_jbsv7_1"
|
||||
class="_switch-wrapper_1a8sn_6"
|
||||
>
|
||||
<button
|
||||
aria-checked="true"
|
||||
class="_switch_jbsv7_1"
|
||||
class="_switch_1a8sn_6"
|
||||
data-color="robin"
|
||||
data-state="checked"
|
||||
id=":r0:"
|
||||
@@ -35,7 +35,7 @@ exports[`PipelinePage container test should render DragAction section 1`] = `
|
||||
value="on"
|
||||
>
|
||||
<span
|
||||
class="_switch__thumb_jbsv7_59"
|
||||
class="_switch__thumb_1a8sn_71"
|
||||
data-state="checked"
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -74,7 +74,7 @@ exports[`PipelinePage container test should render PipelinePageLayout section 1`
|
||||
/>
|
||||
<div>
|
||||
<p
|
||||
class="_typography_ulrzs_1"
|
||||
class="_typography_j4pmm_1"
|
||||
data-slot="typography"
|
||||
data-variant="text"
|
||||
>
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
--tab-content-padding: 0px;
|
||||
--tabs-content-padding: 0px;
|
||||
|
||||
[role='tabpanel'] {
|
||||
display: flex;
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
.filterSelect {
|
||||
min-width: 300px;
|
||||
min-width: 400px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -57,8 +57,6 @@
|
||||
|
||||
--tanstack-cell-padding-top-override: 5px;
|
||||
--tanstack-cell-padding-bottom-override: 5px;
|
||||
--tanstack-cell-padding-left-override: 5px;
|
||||
--tanstack-cell-padding-right-override: 5px;
|
||||
|
||||
--tanstack-cell-padding-left-override: 16px;
|
||||
--tanstack-cell-padding-right-override: 16px;
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
is hidden — the row stays a single crisp line and scrolls only when narrow. */
|
||||
.typeTabsScroll {
|
||||
justify-self: flex-end;
|
||||
--tab-list-wrapper-secondary-padding-left: 0;
|
||||
--tabs-list-wrapper-secondary-padding-left: 0;
|
||||
}
|
||||
|
||||
/* Connected segmented control, mirroring Overview's SegmentedControl: no outer
|
||||
|
||||
Reference in New Issue
Block a user