Compare commits

..

1 Commits

Author SHA1 Message Date
Ashwin Bhatkal
e6d11a4da9 fix: auto update lint fixes 2026-04-24 13:25:11 +05:30
597 changed files with 4676 additions and 8460 deletions

View File

@@ -11,8 +11,6 @@ global:
external_url: <unset>
# the url where the SigNoz backend receives telemetry data (traces, metrics, logs) from instrumented applications.
ingestion_url: <unset>
# the url of the SigNoz MCP server. when unset, the MCP settings page is hidden in the frontend.
# mcp_url: <unset>
##################### Version #####################
version:

View File

@@ -2369,13 +2369,6 @@ components:
$ref: '#/components/schemas/GlobaltypesIdentNConfig'
ingestion_url:
type: string
mcp_url:
nullable: true
type: string
required:
- external_url
- ingestion_url
- mcp_url
type: object
GlobaltypesIdentNConfig:
properties:

View File

@@ -614,9 +614,8 @@
"signoz/no-navigator-clipboard": "off",
// Tests can use navigator.clipboard directly,
"signoz/no-raw-absolute-path":"off",
"no-restricted-globals": "off",
"no-restricted-globals": "off"
// Tests need raw localStorage/sessionStorage to seed DOM state for isolation
"signoz/no-zustand-getstate-in-hooks": "off"
}
},
{

View File

@@ -60,7 +60,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
if (org && org.length > 0 && org[0].id !== undefined) {
return org[0];
}
return undefined;
return;
}, [org]);
const { data: usersData, isFetching: isFetchingUsers } = useListUsers({
@@ -192,7 +192,7 @@ function PrivateRoute({ children }: PrivateRouteProps): JSX.Element {
if (isPrivate) {
if (isLoggedInState) {
const route = routePermission[key];
if (route && route.find((e) => e === user.role) === undefined) {
if (route && route.some((e) => e === user.role) === undefined) {
return <Redirect to={ROUTES.UN_AUTHORIZED} />;
}
} else {

View File

@@ -320,12 +320,12 @@ function App(): JSX.Element {
}),
],
// Performance Monitoring
tracesSampleRate: 1.0, // Capture 100% of the transactions
tracesSampleRate: 1, // Capture 100% of the transactions
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: [],
// Session Replay
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.
replaysOnErrorSampleRate: 1, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
beforeSend(event) {
const sessionReplayUrl = posthog.get_session_replay_url?.({
withTimestamp: true,

View File

@@ -24,7 +24,7 @@ export function ErrorResponseHandler(error: AxiosError): ErrorResponse {
const { errors, error } = data;
const errorMessage =
Array.isArray(errors) && errors.length >= 1 ? errors[0].msg : error;
Array.isArray(errors) && errors.length > 0 ? errors[0].msg : error;
return {
statusCode,

View File

@@ -21,12 +21,12 @@ const dashboardVariablesQuery = async (
});
const timeVariables: Record<string, number> = {
start_timestamp_ms: parseInt(start, 10) * 1e3,
end_timestamp_ms: parseInt(end, 10) * 1e3,
start_timestamp_nano: parseInt(start, 10) * 1e9,
end_timestamp_nano: parseInt(end, 10) * 1e9,
start_timestamp: parseInt(start, 10),
end_timestamp: parseInt(end, 10),
start_timestamp_ms: Number.parseInt(start, 10) * 1e3,
end_timestamp_ms: Number.parseInt(end, 10) * 1e3,
start_timestamp_nano: Number.parseInt(start, 10) * 1e9,
end_timestamp_nano: Number.parseInt(end, 10) * 1e9,
start_timestamp: Number.parseInt(start, 10),
end_timestamp: Number.parseInt(end, 10),
};
const payload = { ...props };

View File

@@ -104,7 +104,7 @@ describe('getFieldKeys API', () => {
const result = await getFieldKeys('traces');
// Verify the returned structure matches SuccessResponseV2 format
expect(result).toEqual({
expect(result).toStrictEqual({
httpStatusCode: 200,
data: mockSuccessResponse.data.data,
});

View File

@@ -199,7 +199,7 @@ describe('getFieldValues API', () => {
const result = await getFieldValues('traces', 'service.name');
// Verify the returned structure matches SuccessResponseV2 format
expect(result).toEqual({
expect(result).toStrictEqual({
httpStatusCode: 200,
data: expect.objectContaining({
values: expect.any(Object),

View File

@@ -3125,17 +3125,12 @@ export interface GlobaltypesConfigDTO {
/**
* @type string
*/
external_url: string;
external_url?: string;
identN?: GlobaltypesIdentNConfigDTO;
/**
* @type string
*/
ingestion_url: string;
/**
* @type string
* @nullable true
*/
mcp_url: string | null;
ingestion_url?: string;
}
export interface GlobaltypesIdentNConfigDTO {

View File

@@ -32,7 +32,7 @@ export const interceptorsResponse = (
): Promise<AxiosResponse<any>> => {
if ((value.config as any)?.metadata) {
const duration =
new Date().getTime() - (value.config as any).metadata.startTime;
Date.now() - (value.config as any).metadata.startTime;
if (duration > RESPONSE_TIMEOUT_THRESHOLD && value.config.url !== '/event') {
eventEmitter.emit(Events.SLOW_API_WARNING, true, {
@@ -55,7 +55,7 @@ export const interceptorsRequestResponse = (
): InternalAxiosRequestConfig => {
// Attach metadata safely (not sent with the request)
Object.defineProperty(value, 'metadata', {
value: { startTime: new Date().getTime() },
value: { startTime: Date.now() },
enumerable: false, // Prevents it from being included in the request
});
@@ -143,7 +143,7 @@ export const interceptorRejected = async (
Logout();
}
}
} catch (error) {
} catch {
Logout();
}
}
@@ -160,7 +160,7 @@ export const interceptorRejected = async (
const interceptorRejectedBase = async (
value: AxiosResponse<any>,
): Promise<AxiosResponse<any>> => Promise.reject(value);
): Promise<AxiosResponse<any>> => { throw value; };
const instance = axios.create({
baseURL: `${ENVIRONMENT.baseURL}${apiV1}`,

View File

@@ -76,10 +76,10 @@ describe('interceptorRejected', () => {
}
const mockAxiosFn = (axios as unknown) as jest.Mock;
expect(mockAxiosFn.mock.calls.length).toBe(1);
expect(mockAxiosFn.mock.calls).toHaveLength(1);
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
expect(Array.isArray(JSON.parse(retryCallConfig.data))).toBe(true);
expect(JSON.parse(retryCallConfig.data)).toEqual(arrayPayload);
expect(JSON.parse(retryCallConfig.data)).toStrictEqual(arrayPayload);
});
it('should preserve object payload structure when retrying a 401 request', async () => {
@@ -112,9 +112,9 @@ describe('interceptorRejected', () => {
}
const mockAxiosFn = (axios as unknown) as jest.Mock;
expect(mockAxiosFn.mock.calls.length).toBe(1);
expect(mockAxiosFn.mock.calls).toHaveLength(1);
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
expect(JSON.parse(retryCallConfig.data)).toEqual(objectPayload);
expect(JSON.parse(retryCallConfig.data)).toStrictEqual(objectPayload);
});
it('should handle undefined data gracefully when retrying', async () => {
@@ -145,7 +145,7 @@ describe('interceptorRejected', () => {
}
const mockAxiosFn = (axios as unknown) as jest.Mock;
expect(mockAxiosFn.mock.calls.length).toBe(1);
expect(mockAxiosFn.mock.calls).toHaveLength(1);
const retryCallConfig = mockAxiosFn.mock.calls[0][0];
expect(retryCallConfig.data).toBeUndefined();
});

View File

@@ -37,11 +37,11 @@ export const downloadExportData = async (
const filename =
response.headers['content-disposition']
?.split('filename=')[1]
?.replace(/["']/g, '') || `exported_data.${props.format || 'txt'}`;
?.replaceAll(/["']/g, '') || `exported_data.${props.format || 'txt'}`;
link.setAttribute('download', filename);
document.body.appendChild(link);
document.body.append(link);
link.click();
link.remove();
URL.revokeObjectURL(url);

View File

@@ -101,7 +101,7 @@ describe('convertV5ResponseToLegacy', () => {
const q = result.payload.data.result[0];
expect(q.queryName).toBe('A');
expect(q.legend).toBe('{{service.name}}');
expect(q.series?.[0]).toEqual(
expect(q.series?.[0]).toStrictEqual(
expect.objectContaining({
labels: { 'service.name': 'adservice' },
values: [
@@ -190,7 +190,7 @@ describe('convertV5ResponseToLegacy', () => {
expect(result.payload.data.resultType).toBe('scalar');
const [tableEntry] = result.payload.data.result;
expect(tableEntry.table?.columns).toEqual([
expect(tableEntry.table?.columns).toStrictEqual([
{
name: 'service.name',
queryName: 'A',
@@ -206,7 +206,7 @@ describe('convertV5ResponseToLegacy', () => {
},
{ name: 'F1', queryName: 'F1', isValueColumn: true, id: 'F1' },
]);
expect(tableEntry.table?.rows?.[0]).toEqual({
expect(tableEntry.table?.rows?.[0]).toStrictEqual({
data: {
'service.name': 'adservice',
'A.count()': 606,
@@ -263,7 +263,7 @@ describe('convertV5ResponseToLegacy', () => {
expect(result.payload.data.resultType).toBe('scalar');
const [tableEntry] = result.payload.data.result;
expect(tableEntry.table?.columns).toEqual([
expect(tableEntry.table?.columns).toStrictEqual([
{
name: 'service.name',
queryName: 'A',
@@ -273,7 +273,7 @@ describe('convertV5ResponseToLegacy', () => {
// Single aggregation: name resolves to legend, id resolves to queryName
{ name: '{{service.name}}', queryName: 'A', isValueColumn: true, id: 'A' },
]);
expect(tableEntry.table?.rows?.[0]).toEqual({
expect(tableEntry.table?.rows?.[0]).toStrictEqual({
data: {
'service.name': 'adservice',
A: 580,

View File

@@ -85,7 +85,7 @@ function convertTimeSeriesData(
const { index, alias } = aggregation;
const seriesData = aggregation[seriesKey];
if (!seriesData || !seriesData.length) {
if (!seriesData || seriesData.length === 0) {
return [];
}

View File

@@ -104,7 +104,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect(result).toStrictEqual(
expect.objectContaining({
legendMap: { A: 'Legend A', F1: 'Formula Legend' },
queryPayload: expect.objectContaining({
@@ -154,7 +154,7 @@ describe('prepareQueryRangePayloadV5', () => {
);
// Legend map combines builder and formulas
expect(result.legendMap).toEqual({ A: 'Legend A', F1: 'Formula Legend' });
expect(result.legendMap).toStrictEqual({ A: 'Legend A', F1: 'Formula Legend' });
const payload: QueryRangePayloadV5 = result.queryPayload;
@@ -166,7 +166,7 @@ describe('prepareQueryRangePayloadV5', () => {
expect(payload.formatOptions?.fillGaps).toBe(true);
// Variables mapped as { key: { value } }
expect(payload.variables).toEqual({
expect(payload.variables).toStrictEqual({
svc: { value: 'api' },
count: { value: 5 },
flag: { value: true },
@@ -226,7 +226,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect(result).toStrictEqual(
expect.objectContaining({
legendMap: { A: 'LP' },
queryPayload: expect.objectContaining({
@@ -255,7 +255,7 @@ describe('prepareQueryRangePayloadV5', () => {
}),
);
expect(result.legendMap).toEqual({ A: 'LP' });
expect(result.legendMap).toStrictEqual({ A: 'LP' });
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.requestType).toBe('time_series');
@@ -296,7 +296,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect(result).toStrictEqual(
expect.objectContaining({
legendMap: { Q: 'LC' },
queryPayload: expect.objectContaining({
@@ -324,7 +324,7 @@ describe('prepareQueryRangePayloadV5', () => {
}),
);
expect(result.legendMap).toEqual({ Q: 'LC' });
expect(result.legendMap).toStrictEqual({ Q: 'LC' });
const payload: QueryRangePayloadV5 = result.queryPayload;
expect(payload.requestType).toBe('scalar');
@@ -353,7 +353,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect(result).toStrictEqual(
expect.objectContaining({
legendMap: {},
queryPayload: expect.objectContaining({
@@ -397,7 +397,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect(result).toStrictEqual(
expect.objectContaining({
legendMap: { A: 'Legend A' },
queryPayload: expect.objectContaining({
@@ -471,7 +471,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect(result).toStrictEqual(
expect.objectContaining({
legendMap: { A: 'Legend A' },
queryPayload: expect.objectContaining({
@@ -585,7 +585,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result).toEqual(
expect(result).toStrictEqual(
expect.objectContaining({
legendMap: { A: '{{service.name}}' },
queryPayload: expect.objectContaining({
@@ -684,7 +684,7 @@ describe('prepareQueryRangePayloadV5', () => {
const result = prepareQueryRangePayloadV5(props);
expect(result.legendMap).toEqual({ A: 'Legend A' });
expect(result.legendMap).toStrictEqual({ A: 'Legend A' });
expect(result.queryPayload.compositeQuery.queries).toHaveLength(1);
const builderQuery = result.queryPayload.compositeQuery.queries.find(
@@ -694,7 +694,7 @@ describe('prepareQueryRangePayloadV5', () => {
expect(logSpec.name).toBe('A');
expect(logSpec.signal).toBe('logs');
expect(logSpec.filter).toEqual({
expect(logSpec.filter).toStrictEqual({
expression:
"service.name = 'payment-service' AND http.status_code >= 400 AND message contains 'error'",
});
@@ -731,7 +731,7 @@ describe('prepareQueryRangePayloadV5', () => {
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.filter).toEqual({ expression: 'http.status_code >= 500' });
expect(logSpec.filter).toStrictEqual({ expression: 'http.status_code >= 500' });
});
it('derives expression from filters when filter is undefined', () => {
@@ -775,7 +775,7 @@ describe('prepareQueryRangePayloadV5', () => {
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.filter).toEqual({ expression: "service.name = 'checkout'" });
expect(logSpec.filter).toStrictEqual({ expression: "service.name = 'checkout'" });
});
it('prefers filter.expression over filters when both are present', () => {
@@ -819,7 +819,7 @@ describe('prepareQueryRangePayloadV5', () => {
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.filter).toEqual({ expression: "service.name = 'frontend'" });
expect(logSpec.filter).toStrictEqual({ expression: "service.name = 'frontend'" });
});
it('returns empty expression when neither filter nor filters provided', () => {
@@ -853,7 +853,7 @@ describe('prepareQueryRangePayloadV5', () => {
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.filter).toEqual({ expression: '' });
expect(logSpec.filter).toStrictEqual({ expression: '' });
});
it('returns empty expression when filters provided with empty items', () => {
@@ -887,6 +887,6 @@ describe('prepareQueryRangePayloadV5', () => {
(q) => q.type === 'builder_query',
) as QueryEnvelope;
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.filter).toEqual({ expression: '' });
expect(logSpec.filter).toStrictEqual({ expression: '' });
});
});

View File

@@ -243,7 +243,7 @@ export function parseAggregations(
let alias = match[2] || availableAlias; // Use provided alias or availableAlias if not matched
if (alias) {
// Remove quotes if present
alias = alias.replace(/^['"]|['"]$/g, '');
alias = alias.replaceAll(/^['"]|['"]$/g, '');
result.push({ expression: expr, alias });
} else {
result.push({ expression: expr });
@@ -634,8 +634,8 @@ export const prepareQueryRangePayloadV5 = ({
// Create V5 payload
const queryPayload: QueryRangePayloadV5 = {
schemaVersion: 'v1',
start: startTime ? startTime * 1e3 : parseInt(start, 10) * 1e3,
end: endTime ? endTime * 1e3 : parseInt(end, 10) * 1e3,
start: startTime ? startTime * 1e3 : Number.parseInt(start, 10) * 1e3,
end: endTime ? endTime * 1e3 : Number.parseInt(end, 10) * 1e3,
requestType,
compositeQuery: {
queries,

View File

@@ -1,70 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 142.5 145.6" style="enable-background:new 0 0 142.5 145.6;" xml:space="preserve">
<style type="text/css">
.st0{fill:#565656;}
.st1{fill:url(#SVGID_1_);}
</style>
<g>
<path class="st0" d="M28.7,131.5c-0.3,7.9-6.6,14.1-14.4,14.1C6.1,145.6,0,139,0,130.9s6.6-14.7,14.7-14.7c3.6,0,7.2,1.6,10.2,4.4
l-2.3,2.9c-2.3-2-5.1-3.4-7.9-3.4c-5.9,0-10.8,4.8-10.8,10.8c0,6.1,4.6,10.8,10.4,10.8c5.2,0,9.3-3.8,10.2-8.8H12.6v-3.5h16.1
V131.5z"/>
<path class="st0" d="M42.3,129.5h-2.2c-2.4,0-4.4,2-4.4,4.4v11.4h-3.9v-19.6H35v1.6c1.1-1.1,2.7-1.6,4.6-1.6h4.2L42.3,129.5z"/>
<path class="st0" d="M63.7,145.3h-3.4v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4V145.3z M59.7,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
C57.1,141.2,59.1,139.3,59.7,137z"/>
<path class="st0" d="M71.5,124.7v1.1h6.2v3.4h-6.2v16.1h-3.8v-20.5c0-4.3,3.1-6.8,7-6.8h4.7l-1.6,3.7h-3.1
C72.9,121.6,71.5,123,71.5,124.7z"/>
<path class="st0" d="M98.5,145.3h-3.3v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4v19.6H98.5z M94.5,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
C92,141.2,93.9,139.3,94.5,137z"/>
<path class="st0" d="M119.4,133.8v11.5h-3.9v-11.6c0-2.4-2-4.4-4.4-4.4c-2.5,0-4.4,2-4.4,4.4v11.6h-3.9v-19.6h3.2v1.7
c1.4-1.3,3.3-2,5.2-2C115.8,125.5,119.4,129.2,119.4,133.8z"/>
<path class="st0" d="M142.4,145.3h-3.3v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4v19.6H142.4z M138.4,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
C135.9,141.2,137.8,139.3,138.4,137z"/>
</g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="71.25" y1="10.4893" x2="71.25" y2="113.3415" gradientTransform="matrix(1 0 0 -1 0 148.6)">
<stop offset="0" style="stop-color:#FCEE1F"/>
<stop offset="1" style="stop-color:#F15B2A"/>
</linearGradient>
<path class="st1" d="M122.9,49.9c-0.2-1.9-0.5-4.1-1.1-6.5c-0.6-2.4-1.6-5-2.9-7.8c-1.4-2.7-3.1-5.6-5.4-8.3
c-0.9-1.1-1.9-2.1-2.9-3.2c1.6-6.3-1.9-11.8-1.9-11.8c-6.1-0.4-9.9,1.9-11.3,2.9c-0.2-0.1-0.5-0.2-0.7-0.3c-1-0.4-2.1-0.8-3.2-1.2
c-1.1-0.3-2.2-0.7-3.3-0.9c-1.1-0.3-2.3-0.5-3.5-0.7c-0.2,0-0.4-0.1-0.6-0.1C83.5,3.6,75.9,0,75.9,0c-8.7,5.6-10.4,13.1-10.4,13.1
s0,0.2-0.1,0.4c-0.5,0.1-0.9,0.3-1.4,0.4c-0.6,0.2-1.3,0.4-1.9,0.7c-0.6,0.3-1.3,0.5-1.9,0.8c-1.3,0.6-2.5,1.2-3.8,1.9
c-1.2,0.7-2.4,1.4-3.5,2.2c-0.2-0.1-0.3-0.2-0.3-0.2c-11.7-4.5-22.1,0.9-22.1,0.9c-0.9,12.5,4.7,20.3,5.8,21.7
c-0.3,0.8-0.5,1.5-0.8,2.3c-0.9,2.8-1.5,5.7-1.9,8.7c-0.1,0.4-0.1,0.9-0.2,1.3c-10.8,5.3-14,16.3-14,16.3c9,10.4,19.6,11,19.6,11
l0,0c1.3,2.4,2.9,4.7,4.6,6.8c0.7,0.9,1.5,1.7,2.3,2.6c-3.3,9.4,0.5,17.3,0.5,17.3c10.1,0.4,16.7-4.4,18.1-5.5c1,0.3,2,0.6,3,0.9
c3.1,0.8,6.3,1.3,9.4,1.4c0.8,0,1.6,0,2.4,0h0.4H80h0.5H81l0,0c4.7,6.8,13.1,7.7,13.1,7.7c5.9-6.3,6.3-12.4,6.3-13.8l0,0
c0,0,0,0,0-0.1s0-0.2,0-0.2l0,0c0-0.1,0-0.2,0-0.3c1.2-0.9,2.4-1.8,3.6-2.8c2.4-2.1,4.4-4.6,6.2-7.2c0.2-0.2,0.3-0.5,0.5-0.7
c6.7,0.4,11.4-4.2,11.4-4.2c-1.1-7-5.1-10.4-5.9-11l0,0c0,0,0,0-0.1-0.1l-0.1-0.1l0,0l-0.1-0.1c0-0.4,0.1-0.8,0.1-1.3
c0.1-0.8,0.1-1.5,0.1-2.3v-0.6v-0.3v-0.1c0-0.2,0-0.1,0-0.2v-0.5v-0.6c0-0.2,0-0.4,0-0.6s0-0.4-0.1-0.6l-0.1-0.6l-0.1-0.6
c-0.1-0.8-0.3-1.5-0.4-2.3c-0.7-3-1.9-5.9-3.4-8.4c-1.6-2.6-3.5-4.8-5.7-6.8c-2.2-1.9-4.6-3.5-7.2-4.6c-2.6-1.2-5.2-1.9-7.9-2.2
c-1.3-0.2-2.7-0.2-4-0.2h-0.5h-0.1h-0.2h-0.2h-0.5c-0.2,0-0.4,0-0.5,0c-0.7,0.1-1.4,0.2-2,0.3c-2.7,0.5-5.2,1.5-7.4,2.8
c-2.2,1.3-4.1,3-5.7,4.9s-2.8,3.9-3.6,6.1c-0.8,2.1-1.3,4.4-1.4,6.5c0,0.5,0,1.1,0,1.6c0,0.1,0,0.3,0,0.4v0.4c0,0.3,0,0.5,0.1,0.8
c0.1,1.1,0.3,2.1,0.6,3.1c0.6,2,1.5,3.8,2.7,5.4s2.5,2.8,4,3.8s3,1.7,4.6,2.2c1.6,0.5,3.1,0.7,4.5,0.6c0.2,0,0.4,0,0.5,0
c0.1,0,0.2,0,0.3,0s0.2,0,0.3,0c0.2,0,0.3,0,0.5,0h0.1h0.1c0.1,0,0.2,0,0.3,0c0.2,0,0.4-0.1,0.5-0.1c0.2,0,0.3-0.1,0.5-0.1
c0.3-0.1,0.7-0.2,1-0.3c0.6-0.2,1.2-0.5,1.8-0.7c0.6-0.3,1.1-0.6,1.5-0.9c0.1-0.1,0.3-0.2,0.4-0.3c0.5-0.4,0.6-1.1,0.2-1.6
c-0.4-0.4-1-0.5-1.5-0.3C88,74,87.9,74,87.7,74.1c-0.4,0.2-0.9,0.4-1.3,0.5c-0.5,0.1-1,0.3-1.5,0.4c-0.3,0-0.5,0.1-0.8,0.1
c-0.1,0-0.3,0-0.4,0c-0.1,0-0.3,0-0.4,0s-0.3,0-0.4,0c-0.2,0-0.3,0-0.5,0c0,0-0.1,0,0,0h-0.1h-0.1c-0.1,0-0.1,0-0.2,0
s-0.3,0-0.4-0.1c-1.1-0.2-2.3-0.5-3.4-1c-1.1-0.5-2.2-1.2-3.1-2.1c-1-0.9-1.8-1.9-2.5-3.1c-0.7-1.2-1.1-2.5-1.3-3.8
c-0.1-0.7-0.2-1.4-0.1-2.1c0-0.2,0-0.4,0-0.6c0,0.1,0,0,0,0v-0.1v-0.1c0-0.1,0-0.2,0-0.3c0-0.4,0.1-0.7,0.2-1.1c0.5-3,2-5.9,4.3-8.1
c0.6-0.6,1.2-1.1,1.9-1.5c0.7-0.5,1.4-0.9,2.1-1.2c0.7-0.3,1.5-0.6,2.3-0.8s1.6-0.4,2.4-0.4c0.4,0,0.8-0.1,1.2-0.1
c0.1,0,0.2,0,0.3,0h0.3h0.2c0.1,0,0,0,0,0h0.1h0.3c0.9,0.1,1.8,0.2,2.6,0.4c1.7,0.4,3.4,1,5,1.9c3.2,1.8,5.9,4.5,7.5,7.8
c0.8,1.6,1.4,3.4,1.7,5.3c0.1,0.5,0.1,0.9,0.2,1.4v0.3V66c0,0.1,0,0.2,0,0.3c0,0.1,0,0.2,0,0.3v0.3v0.3c0,0.2,0,0.6,0,0.8
c0,0.5-0.1,1-0.1,1.5c-0.1,0.5-0.1,1-0.2,1.5s-0.2,1-0.3,1.5c-0.2,1-0.6,1.9-0.9,2.9c-0.7,1.9-1.7,3.7-2.9,5.3
c-2.4,3.3-5.7,6-9.4,7.7c-1.9,0.8-3.8,1.5-5.8,1.8c-1,0.2-2,0.3-3,0.3H81h-0.2h-0.3H80h-0.3c0.1,0,0,0,0,0h-0.1
c-0.5,0-1.1,0-1.6-0.1c-2.2-0.2-4.3-0.6-6.4-1.2c-2.1-0.6-4.1-1.4-6-2.4c-3.8-2-7.2-4.9-9.9-8.2c-1.3-1.7-2.5-3.5-3.5-5.4
s-1.7-3.9-2.3-5.9c-0.6-2-0.9-4.1-1-6.2v-0.4v-0.1v-0.1v-0.2V60v-0.1v-0.1v-0.2v-0.5V59l0,0v-0.2c0-0.3,0-0.5,0-0.8
c0-1,0.1-2.1,0.3-3.2c0.1-1.1,0.3-2.1,0.5-3.2c0.2-1.1,0.5-2.1,0.8-3.2c0.6-2.1,1.3-4.1,2.2-6c1.8-3.8,4.1-7.2,6.8-9.9
c0.7-0.7,1.4-1.3,2.2-1.9c0.3-0.3,1-0.9,1.8-1.4c0.8-0.5,1.6-1,2.5-1.4c0.4-0.2,0.8-0.4,1.3-0.6c0.2-0.1,0.4-0.2,0.7-0.3
c0.2-0.1,0.4-0.2,0.7-0.3c0.9-0.4,1.8-0.7,2.7-1c0.2-0.1,0.5-0.1,0.7-0.2c0.2-0.1,0.5-0.1,0.7-0.2c0.5-0.1,0.9-0.2,1.4-0.4
c0.2-0.1,0.5-0.1,0.7-0.2c0.2,0,0.5-0.1,0.7-0.1c0.2,0,0.5-0.1,0.7-0.1l0.4-0.1l0.4-0.1c0.2,0,0.5-0.1,0.7-0.1
c0.3,0,0.5-0.1,0.8-0.1c0.2,0,0.6-0.1,0.8-0.1c0.2,0,0.3,0,0.5-0.1h0.3h0.2h0.2c0.3,0,0.5,0,0.8-0.1h0.4c0,0,0.1,0,0,0h0.1h0.2
c0.2,0,0.5,0,0.7,0c0.9,0,1.8,0,2.7,0c1.8,0.1,3.6,0.3,5.3,0.6c3.4,0.6,6.7,1.7,9.6,3.2c2.9,1.4,5.6,3.2,7.8,5.1
c0.1,0.1,0.3,0.2,0.4,0.4c0.1,0.1,0.3,0.2,0.4,0.4c0.3,0.2,0.5,0.5,0.8,0.7c0.3,0.2,0.5,0.5,0.8,0.7c0.2,0.3,0.5,0.5,0.7,0.8
c1,1,1.9,2.1,2.7,3.1c1.6,2.1,2.9,4.2,3.9,6.2c0.1,0.1,0.1,0.2,0.2,0.4c0.1,0.1,0.1,0.2,0.2,0.4s0.2,0.5,0.4,0.7
c0.1,0.2,0.2,0.5,0.3,0.7c0.1,0.2,0.2,0.5,0.3,0.7c0.4,0.9,0.7,1.8,1,2.7c0.5,1.4,0.8,2.6,1.1,3.6c0.1,0.4,0.5,0.7,0.9,0.7
c0.5,0,0.8-0.4,0.8-0.9C123,52.7,123,51.4,122.9,49.9z"/>
</svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 21.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 142.5 145.6" style="enable-background:new 0 0 142.5 145.6;" xml:space="preserve">
<style type="text/css">
.st0{fill:#565656;}
.st1{fill:url(#SVGID_1_);}
</style>
<g>
<path class="st0" d="M28.7,131.5c-0.3,7.9-6.6,14.1-14.4,14.1C6.1,145.6,0,139,0,130.9s6.6-14.7,14.7-14.7c3.6,0,7.2,1.6,10.2,4.4
l-2.3,2.9c-2.3-2-5.1-3.4-7.9-3.4c-5.9,0-10.8,4.8-10.8,10.8c0,6.1,4.6,10.8,10.4,10.8c5.2,0,9.3-3.8,10.2-8.8H12.6v-3.5h16.1
V131.5z"/>
<path class="st0" d="M42.3,129.5h-2.2c-2.4,0-4.4,2-4.4,4.4v11.4h-3.9v-19.6H35v1.6c1.1-1.1,2.7-1.6,4.6-1.6h4.2L42.3,129.5z"/>
<path class="st0" d="M63.7,145.3h-3.4v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4V145.3z M59.7,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
C57.1,141.2,59.1,139.3,59.7,137z"/>
<path class="st0" d="M71.5,124.7v1.1h6.2v3.4h-6.2v16.1h-3.8v-20.5c0-4.3,3.1-6.8,7-6.8h4.7l-1.6,3.7h-3.1
C72.9,121.6,71.5,123,71.5,124.7z"/>
<path class="st0" d="M98.5,145.3h-3.3v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4v19.6H98.5z M94.5,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
C92,141.2,93.9,139.3,94.5,137z"/>
<path class="st0" d="M119.4,133.8v11.5h-3.9v-11.6c0-2.4-2-4.4-4.4-4.4c-2.5,0-4.4,2-4.4,4.4v11.6h-3.9v-19.6h3.2v1.7
c1.4-1.3,3.3-2,5.2-2C115.8,125.5,119.4,129.2,119.4,133.8z"/>
<path class="st0" d="M142.4,145.3h-3.3v-2.5c-2.6,2.5-6.6,3.7-10.7,1.9c-3-1.3-5.3-4.1-5.9-7.4c-1.2-6.3,3.7-11.9,9.9-11.9
c2.6,0,5,1.1,6.7,2.8v-2.5h3.4v19.6H142.4z M138.4,137c0.9-4-2.1-7.6-6-7.6c-3.4,0-6.1,2.8-6.1,6.1c0,3.8,3.3,6.7,7.2,6.1
C135.9,141.2,137.8,139.3,138.4,137z"/>
</g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="71.25" y1="10.4893" x2="71.25" y2="113.3415" gradientTransform="matrix(1 0 0 -1 0 148.6)">
<stop offset="0" style="stop-color:#FCEE1F"/>
<stop offset="1" style="stop-color:#F15B2A"/>
</linearGradient>
<path class="st1" d="M122.9,49.9c-0.2-1.9-0.5-4.1-1.1-6.5c-0.6-2.4-1.6-5-2.9-7.8c-1.4-2.7-3.1-5.6-5.4-8.3
c-0.9-1.1-1.9-2.1-2.9-3.2c1.6-6.3-1.9-11.8-1.9-11.8c-6.1-0.4-9.9,1.9-11.3,2.9c-0.2-0.1-0.5-0.2-0.7-0.3c-1-0.4-2.1-0.8-3.2-1.2
c-1.1-0.3-2.2-0.7-3.3-0.9c-1.1-0.3-2.3-0.5-3.5-0.7c-0.2,0-0.4-0.1-0.6-0.1C83.5,3.6,75.9,0,75.9,0c-8.7,5.6-10.4,13.1-10.4,13.1
s0,0.2-0.1,0.4c-0.5,0.1-0.9,0.3-1.4,0.4c-0.6,0.2-1.3,0.4-1.9,0.7c-0.6,0.3-1.3,0.5-1.9,0.8c-1.3,0.6-2.5,1.2-3.8,1.9
c-1.2,0.7-2.4,1.4-3.5,2.2c-0.2-0.1-0.3-0.2-0.3-0.2c-11.7-4.5-22.1,0.9-22.1,0.9c-0.9,12.5,4.7,20.3,5.8,21.7
c-0.3,0.8-0.5,1.5-0.8,2.3c-0.9,2.8-1.5,5.7-1.9,8.7c-0.1,0.4-0.1,0.9-0.2,1.3c-10.8,5.3-14,16.3-14,16.3c9,10.4,19.6,11,19.6,11
l0,0c1.3,2.4,2.9,4.7,4.6,6.8c0.7,0.9,1.5,1.7,2.3,2.6c-3.3,9.4,0.5,17.3,0.5,17.3c10.1,0.4,16.7-4.4,18.1-5.5c1,0.3,2,0.6,3,0.9
c3.1,0.8,6.3,1.3,9.4,1.4c0.8,0,1.6,0,2.4,0h0.4H80h0.5H81l0,0c4.7,6.8,13.1,7.7,13.1,7.7c5.9-6.3,6.3-12.4,6.3-13.8l0,0
c0,0,0,0,0-0.1s0-0.2,0-0.2l0,0c0-0.1,0-0.2,0-0.3c1.2-0.9,2.4-1.8,3.6-2.8c2.4-2.1,4.4-4.6,6.2-7.2c0.2-0.2,0.3-0.5,0.5-0.7
c6.7,0.4,11.4-4.2,11.4-4.2c-1.1-7-5.1-10.4-5.9-11l0,0c0,0,0,0-0.1-0.1l-0.1-0.1l0,0l-0.1-0.1c0-0.4,0.1-0.8,0.1-1.3
c0.1-0.8,0.1-1.5,0.1-2.3v-0.6v-0.3v-0.1c0-0.2,0-0.1,0-0.2v-0.5v-0.6c0-0.2,0-0.4,0-0.6s0-0.4-0.1-0.6l-0.1-0.6l-0.1-0.6
c-0.1-0.8-0.3-1.5-0.4-2.3c-0.7-3-1.9-5.9-3.4-8.4c-1.6-2.6-3.5-4.8-5.7-6.8c-2.2-1.9-4.6-3.5-7.2-4.6c-2.6-1.2-5.2-1.9-7.9-2.2
c-1.3-0.2-2.7-0.2-4-0.2h-0.5h-0.1h-0.2h-0.2h-0.5c-0.2,0-0.4,0-0.5,0c-0.7,0.1-1.4,0.2-2,0.3c-2.7,0.5-5.2,1.5-7.4,2.8
c-2.2,1.3-4.1,3-5.7,4.9s-2.8,3.9-3.6,6.1c-0.8,2.1-1.3,4.4-1.4,6.5c0,0.5,0,1.1,0,1.6c0,0.1,0,0.3,0,0.4v0.4c0,0.3,0,0.5,0.1,0.8
c0.1,1.1,0.3,2.1,0.6,3.1c0.6,2,1.5,3.8,2.7,5.4s2.5,2.8,4,3.8s3,1.7,4.6,2.2c1.6,0.5,3.1,0.7,4.5,0.6c0.2,0,0.4,0,0.5,0
c0.1,0,0.2,0,0.3,0s0.2,0,0.3,0c0.2,0,0.3,0,0.5,0h0.1h0.1c0.1,0,0.2,0,0.3,0c0.2,0,0.4-0.1,0.5-0.1c0.2,0,0.3-0.1,0.5-0.1
c0.3-0.1,0.7-0.2,1-0.3c0.6-0.2,1.2-0.5,1.8-0.7c0.6-0.3,1.1-0.6,1.5-0.9c0.1-0.1,0.3-0.2,0.4-0.3c0.5-0.4,0.6-1.1,0.2-1.6
c-0.4-0.4-1-0.5-1.5-0.3C88,74,87.9,74,87.7,74.1c-0.4,0.2-0.9,0.4-1.3,0.5c-0.5,0.1-1,0.3-1.5,0.4c-0.3,0-0.5,0.1-0.8,0.1
c-0.1,0-0.3,0-0.4,0c-0.1,0-0.3,0-0.4,0s-0.3,0-0.4,0c-0.2,0-0.3,0-0.5,0c0,0-0.1,0,0,0h-0.1h-0.1c-0.1,0-0.1,0-0.2,0
s-0.3,0-0.4-0.1c-1.1-0.2-2.3-0.5-3.4-1c-1.1-0.5-2.2-1.2-3.1-2.1c-1-0.9-1.8-1.9-2.5-3.1c-0.7-1.2-1.1-2.5-1.3-3.8
c-0.1-0.7-0.2-1.4-0.1-2.1c0-0.2,0-0.4,0-0.6c0,0.1,0,0,0,0v-0.1v-0.1c0-0.1,0-0.2,0-0.3c0-0.4,0.1-0.7,0.2-1.1c0.5-3,2-5.9,4.3-8.1
c0.6-0.6,1.2-1.1,1.9-1.5c0.7-0.5,1.4-0.9,2.1-1.2c0.7-0.3,1.5-0.6,2.3-0.8s1.6-0.4,2.4-0.4c0.4,0,0.8-0.1,1.2-0.1
c0.1,0,0.2,0,0.3,0h0.3h0.2c0.1,0,0,0,0,0h0.1h0.3c0.9,0.1,1.8,0.2,2.6,0.4c1.7,0.4,3.4,1,5,1.9c3.2,1.8,5.9,4.5,7.5,7.8
c0.8,1.6,1.4,3.4,1.7,5.3c0.1,0.5,0.1,0.9,0.2,1.4v0.3V66c0,0.1,0,0.2,0,0.3c0,0.1,0,0.2,0,0.3v0.3v0.3c0,0.2,0,0.6,0,0.8
c0,0.5-0.1,1-0.1,1.5c-0.1,0.5-0.1,1-0.2,1.5s-0.2,1-0.3,1.5c-0.2,1-0.6,1.9-0.9,2.9c-0.7,1.9-1.7,3.7-2.9,5.3
c-2.4,3.3-5.7,6-9.4,7.7c-1.9,0.8-3.8,1.5-5.8,1.8c-1,0.2-2,0.3-3,0.3H81h-0.2h-0.3H80h-0.3c0.1,0,0,0,0,0h-0.1
c-0.5,0-1.1,0-1.6-0.1c-2.2-0.2-4.3-0.6-6.4-1.2c-2.1-0.6-4.1-1.4-6-2.4c-3.8-2-7.2-4.9-9.9-8.2c-1.3-1.7-2.5-3.5-3.5-5.4
s-1.7-3.9-2.3-5.9c-0.6-2-0.9-4.1-1-6.2v-0.4v-0.1v-0.1v-0.2V60v-0.1v-0.1v-0.2v-0.5V59l0,0v-0.2c0-0.3,0-0.5,0-0.8
c0-1,0.1-2.1,0.3-3.2c0.1-1.1,0.3-2.1,0.5-3.2c0.2-1.1,0.5-2.1,0.8-3.2c0.6-2.1,1.3-4.1,2.2-6c1.8-3.8,4.1-7.2,6.8-9.9
c0.7-0.7,1.4-1.3,2.2-1.9c0.3-0.3,1-0.9,1.8-1.4c0.8-0.5,1.6-1,2.5-1.4c0.4-0.2,0.8-0.4,1.3-0.6c0.2-0.1,0.4-0.2,0.7-0.3
c0.2-0.1,0.4-0.2,0.7-0.3c0.9-0.4,1.8-0.7,2.7-1c0.2-0.1,0.5-0.1,0.7-0.2c0.2-0.1,0.5-0.1,0.7-0.2c0.5-0.1,0.9-0.2,1.4-0.4
c0.2-0.1,0.5-0.1,0.7-0.2c0.2,0,0.5-0.1,0.7-0.1c0.2,0,0.5-0.1,0.7-0.1l0.4-0.1l0.4-0.1c0.2,0,0.5-0.1,0.7-0.1
c0.3,0,0.5-0.1,0.8-0.1c0.2,0,0.6-0.1,0.8-0.1c0.2,0,0.3,0,0.5-0.1h0.3h0.2h0.2c0.3,0,0.5,0,0.8-0.1h0.4c0,0,0.1,0,0,0h0.1h0.2
c0.2,0,0.5,0,0.7,0c0.9,0,1.8,0,2.7,0c1.8,0.1,3.6,0.3,5.3,0.6c3.4,0.6,6.7,1.7,9.6,3.2c2.9,1.4,5.6,3.2,7.8,5.1
c0.1,0.1,0.3,0.2,0.4,0.4c0.1,0.1,0.3,0.2,0.4,0.4c0.3,0.2,0.5,0.5,0.8,0.7c0.3,0.2,0.5,0.5,0.8,0.7c0.2,0.3,0.5,0.5,0.7,0.8
c1,1,1.9,2.1,2.7,3.1c1.6,2.1,2.9,4.2,3.9,6.2c0.1,0.1,0.1,0.2,0.2,0.4c0.1,0.1,0.1,0.2,0.2,0.4s0.2,0.5,0.4,0.7
c0.1,0.2,0.2,0.5,0.3,0.7c0.1,0.2,0.2,0.5,0.3,0.7c0.4,0.9,0.7,1.8,1,2.7c0.5,1.4,0.8,2.6,1.1,3.6c0.1,0.4,0.5,0.7,0.9,0.7
c0.5,0,0.8-0.4,0.8-0.9C123,52.7,123,51.4,122.9,49.9z"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

@@ -1,21 +1,21 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<polygon style="fill:#FFD500;" points="382.395,228.568 291.215,228.568 330.762,10.199 129.603,283.43 220.785,283.43
181.238,501.799 "/>
<g>
<path style="fill:#3D3D3D;" d="M181.234,512c-1.355,0-2.726-0.271-4.033-0.833c-4.357-1.878-6.845-6.514-5.999-11.184
l37.371-206.353h-78.969c-3.846,0-7.367-2.164-9.103-5.597c-1.735-3.433-1.391-7.55,0.889-10.648L322.548,4.153
c2.814-3.822,7.891-5.196,12.25-3.32c4.357,1.878,6.845,6.514,5.999,11.184L303.427,218.37h78.969c3.846,0,7.367,2.164,9.103,5.597
c1.735,3.433,1.391,7.55-0.889,10.648L189.451,507.846C187.481,510.523,184.399,512,181.234,512z M149.777,273.231h71.007
c3.023,0,5.89,1.341,7.828,3.662c1.938,2.32,2.747,5.38,2.208,8.355l-31.704,175.065l163.105-221.545h-71.007
c-3.023,0-5.89-1.341-7.828-3.661c-1.938-2.32-2.747-5.38-2.208-8.355l31.704-175.065L149.777,273.231z"/>
<path style="fill:#3D3D3D;" d="M267.666,171.348c-0.604,0-1.215-0.054-1.829-0.165c-5.543-1.004-9.223-6.31-8.22-11.853l0.923-5.1
c1.003-5.543,6.323-9.225,11.852-8.219c5.543,1.004,9.223,6.31,8.22,11.853l-0.923,5.1
C276.797,167.892,272.503,171.348,267.666,171.348z"/>
<path style="fill:#3D3D3D;" d="M255.455,238.77c-0.604,0-1.215-0.054-1.83-0.165c-5.543-1.004-9.222-6.31-8.218-11.853
l7.037-38.864c1.004-5.543,6.317-9.225,11.854-8.219c5.543,1.004,9.222,6.31,8.219,11.853l-7.037,38.864
C264.587,235.314,260.293,238.77,255.455,238.77z"/>
</g>
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 512 512" xml:space="preserve">
<polygon style="fill:#FFD500;" points="382.395,228.568 291.215,228.568 330.762,10.199 129.603,283.43 220.785,283.43
181.238,501.799 "/>
<g>
<path style="fill:#3D3D3D;" d="M181.234,512c-1.355,0-2.726-0.271-4.033-0.833c-4.357-1.878-6.845-6.514-5.999-11.184
l37.371-206.353h-78.969c-3.846,0-7.367-2.164-9.103-5.597c-1.735-3.433-1.391-7.55,0.889-10.648L322.548,4.153
c2.814-3.822,7.891-5.196,12.25-3.32c4.357,1.878,6.845,6.514,5.999,11.184L303.427,218.37h78.969c3.846,0,7.367,2.164,9.103,5.597
c1.735,3.433,1.391,7.55-0.889,10.648L189.451,507.846C187.481,510.523,184.399,512,181.234,512z M149.777,273.231h71.007
c3.023,0,5.89,1.341,7.828,3.662c1.938,2.32,2.747,5.38,2.208,8.355l-31.704,175.065l163.105-221.545h-71.007
c-3.023,0-5.89-1.341-7.828-3.661c-1.938-2.32-2.747-5.38-2.208-8.355l31.704-175.065L149.777,273.231z"/>
<path style="fill:#3D3D3D;" d="M267.666,171.348c-0.604,0-1.215-0.054-1.829-0.165c-5.543-1.004-9.223-6.31-8.22-11.853l0.923-5.1
c1.003-5.543,6.323-9.225,11.852-8.219c5.543,1.004,9.223,6.31,8.22,11.853l-0.923,5.1
C276.797,167.892,272.503,171.348,267.666,171.348z"/>
<path style="fill:#3D3D3D;" d="M255.455,238.77c-0.604,0-1.215-0.054-1.83-0.165c-5.543-1.004-9.222-6.31-8.218-11.853
l7.037-38.864c1.004-5.543,6.317-9.225,11.854-8.219c5.543,1.004,9.222,6.31,8.219,11.853l-7.037,38.864
C264.587,235.314,260.293,238.77,255.455,238.77z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -13,7 +13,7 @@ function AppLoading(): JSX.Element {
try {
const theme = get(LOCALSTORAGE.THEME);
return theme !== THEME_MODE.LIGHT; // Return true for dark, false for light
} catch (error) {
} catch {
// If localStorage is not available, default to dark theme
return true;
}

View File

@@ -23,7 +23,7 @@ describe('AppLoading', () => {
it('should render loading screen with dark theme by default', () => {
// Mock localStorage to return dark theme (or undefined for default)
mockGet.mockReturnValue(undefined);
mockGet.mockReturnValue();
render(<AppLoading />);
@@ -40,7 +40,7 @@ describe('AppLoading', () => {
it('should have proper structure and content', () => {
// Mock localStorage to return dark theme
mockGet.mockReturnValue(undefined);
mockGet.mockReturnValue();
render(<AppLoading />);

View File

@@ -51,7 +51,7 @@ export function FilterSelect({
// Memoize options to include the typed value if not present
const mergedOptions = useMemo(() => {
if (
!!searchValue.trim().length &&
searchValue.trim().length > 0 &&
!options.some((opt) => opt.value === searchValue)
) {
return [{ value: searchValue, label: searchValue }, ...options];

View File

@@ -57,7 +57,7 @@ export const useGetValueFromWidget = (
return 'Error';
}
const value = parseFloat(
const value = Number.parseFloat(
query.data?.payload?.data?.newResult?.data?.result?.[0]?.series?.[0]
?.values?.[0]?.value || 'NaN',
);

View File

@@ -104,11 +104,11 @@ export const createFiltersFromData = (
op: string;
value: string;
}> => {
const excludeKeys = ['A', 'A_without_unit'];
const excludeKeys = new Set(['A', 'A_without_unit']);
return (
Object.entries(data)
.filter(([key]) => !excludeKeys.includes(key))
.filter(([key]) => !excludeKeys.has(key))
// eslint-disable-next-line sonarjs/no-identical-functions
.map(([key, value]) => ({
id: uuidv4(),

View File

@@ -440,8 +440,8 @@ function ClientSideQBSearch(
const values: Array<string | number | boolean> = [];
const { tagValue } = getTagToken(searchValue);
if (isArray(tagValue)) {
if (!isEmpty(tagValue[tagValue.length - 1])) {
values.push(tagValue[tagValue.length - 1]);
if (!isEmpty(tagValue.at(-1))) {
values.push(tagValue.at(-1));
}
} else if (!isEmpty(tagValue)) {
values.push(tagValue);
@@ -488,7 +488,7 @@ function ClientSideQBSearch(
const computedTagValue =
tag.value &&
Array.isArray(tag.value) &&
tag.value[tag.value.length - 1] === ''
tag.value.at(-1) === ''
? tag.value?.slice(0, -1)
: tag.value ?? '';
filterTags.items.push({
@@ -610,7 +610,7 @@ function ClientSideQBSearch(
searchValue={searchValue}
className={className}
rootClassName="query-builder-search client-side-qb-search"
disabled={!attributeKeys.length}
disabled={attributeKeys.length === 0}
style={selectStyle}
onSearch={handleSearch}
onSelect={handleDropdownSelect}

View File

@@ -147,7 +147,7 @@ function CustomTimePicker({
return `Last ${selectedTime}`;
}
const value = parseInt(match[1], 10);
const value = Number.parseInt(match[1], 10);
const unit = match[2];
// Map unit abbreviations to full words
@@ -312,7 +312,7 @@ function CustomTimePicker({
const match = inputValue.match(/^(\d+)([mhdw])$/) as RegExpMatchArray;
const value = parseInt(match[1], 10);
const value = Number.parseInt(match[1], 10);
const unit = match[2];
const currentTime = dayjs();

View File

@@ -21,7 +21,7 @@ interface RangePickerModalProps {
setIsOpen: Dispatch<SetStateAction<boolean>>;
onCustomDateHandler: (
dateTimeRange: DateTimeRangeType,
lexicalContext?: LexicalContext | undefined,
lexicalContext?: LexicalContext ,
) => void;
selectedTime: string;
onTimeChange?: (

View File

@@ -82,7 +82,7 @@ const createTimezoneEntry = (
name: displayName,
value,
offset,
searchIndex: offset.replace(/ /g, ''),
searchIndex: offset.replaceAll(/ /g, ''),
...(hasDivider && { hasDivider }),
};
};

View File

@@ -13,7 +13,7 @@ import '@testing-library/jest-dom';
import { DownloadFormats, DownloadRowCounts } from './constants';
import DownloadOptionsMenu from './DownloadOptionsMenu';
const mockDownloadExportData = jest.fn().mockResolvedValue(undefined);
const mockDownloadExportData = jest.fn().mockResolvedValue();
jest.mock('api/v1/download/downloadExportData', () => ({
downloadExportData: (...args: any[]): any => mockDownloadExportData(...args),
default: (...args: any[]): any => mockDownloadExportData(...args),
@@ -94,7 +94,7 @@ describe.each([
const testId = `periscope-btn-download-${dataSource}`;
beforeEach(() => {
mockDownloadExportData.mockReset().mockResolvedValue(undefined);
mockDownloadExportData.mockReset().mockResolvedValue();
(message.success as jest.Mock).mockReset();
(message.error as jest.Mock).mockReset();
mockUseQueryBuilder.mockReturnValue({
@@ -213,7 +213,7 @@ describe.each([
const callArgs = mockDownloadExportData.mock.calls[0][0];
const query = callArgs.body.compositeQuery.queries[0];
expect(query.spec.groupBy).toBeUndefined();
expect(query.spec.having).toEqual({ expression: '' });
expect(query.spec.having).toStrictEqual({ expression: '' });
});
});
@@ -238,7 +238,7 @@ describe.each([
expect(mockDownloadExportData).toHaveBeenCalledTimes(1);
const callArgs = mockDownloadExportData.mock.calls[0][0];
const query = callArgs.body.compositeQuery.queries[0];
expect(query.spec.selectFields).toEqual([
expect(query.spec.selectFields).toStrictEqual([
expect.objectContaining({
name: 'http.status',
fieldDataType: 'int64',
@@ -322,7 +322,7 @@ describe('DownloadOptionsMenu for traces with queryTraceOperator', () => {
const testId = `periscope-btn-download-${dataSource}`;
beforeEach(() => {
mockDownloadExportData.mockReset().mockResolvedValue(undefined);
mockDownloadExportData.mockReset().mockResolvedValue();
(message.success as jest.Mock).mockReset();
});

View File

@@ -6,39 +6,39 @@ jest.mock('react-dnd', () => ({
}));
describe('Utils testing of DraggableTableRow component', () => {
test('Should dropHandler return true', () => {
it('Should dropHandler return true', () => {
const monitor = {
isOver: jest.fn().mockReturnValueOnce(true),
} as never;
const dropDataTruthy = dropHandler(monitor);
expect(dropDataTruthy).toEqual({ isOver: true });
expect(dropDataTruthy).toStrictEqual({ isOver: true });
});
test('Should dropHandler return false', () => {
it('Should dropHandler return false', () => {
const monitor = {
isOver: jest.fn().mockReturnValueOnce(false),
} as never;
const dropDataFalsy = dropHandler(monitor);
expect(dropDataFalsy).toEqual({ isOver: false });
expect(dropDataFalsy).toStrictEqual({ isOver: false });
});
test('Should dragHandler return true', () => {
it('Should dragHandler return true', () => {
const monitor = {
isDragging: jest.fn().mockReturnValueOnce(true),
} as never;
const dragDataTruthy = dragHandler(monitor);
expect(dragDataTruthy).toEqual({ isDragging: true });
expect(dragDataTruthy).toStrictEqual({ isDragging: true });
});
test('Should dragHandler return false', () => {
it('Should dragHandler return false', () => {
const monitor = {
isDragging: jest.fn().mockReturnValueOnce(false),
} as never;
const dragDataFalsy = dragHandler(monitor);
expect(dragDataFalsy).toEqual({ isDragging: false });
expect(dragDataFalsy).toStrictEqual({ isDragging: false });
});
});

View File

@@ -361,12 +361,10 @@ describe('EditMemberDrawer', () => {
await user.click(screen.getByRole('button', { name: /delete member/i }));
expect(
await screen.findByText(/are you sure you want to delete/i),
).toBeInTheDocument();
await expect(screen.findByText(/are you sure you want to delete/i)).resolves.toBeInTheDocument();
const confirmBtns = screen.getAllByRole('button', { name: /delete member/i });
await user.click(confirmBtns[confirmBtns.length - 1]);
await user.click(confirmBtns.at(-1));
await waitFor(() => {
expect(mockDeleteMutate).toHaveBeenCalledWith({
@@ -441,12 +439,10 @@ describe('EditMemberDrawer', () => {
await user.click(screen.getByRole('button', { name: /revoke invite/i }));
expect(
await screen.findByText(/Are you sure you want to revoke the invite/i),
).toBeInTheDocument();
await expect(screen.findByText(/Are you sure you want to revoke the invite/i)).resolves.toBeInTheDocument();
const confirmBtns = screen.getAllByRole('button', { name: /revoke invite/i });
await user.click(confirmBtns[confirmBtns.length - 1]);
await user.click(confirmBtns.at(-1));
await waitFor(() => {
expect(mockDeleteMutate).toHaveBeenCalledWith({
@@ -553,7 +549,7 @@ describe('EditMemberDrawer', () => {
const confirmBtns = screen.getAllByRole('button', {
name: /delete member/i,
});
await user.click(confirmBtns[confirmBtns.length - 1]);
await user.click(confirmBtns.at(-1));
await waitFor(() => {
expect(showErrorModal).toHaveBeenCalledWith(
@@ -584,7 +580,7 @@ describe('EditMemberDrawer', () => {
const confirmBtns = screen.getAllByRole('button', {
name: /revoke invite/i,
});
await user.click(confirmBtns[confirmBtns.length - 1]);
await user.click(confirmBtns.at(-1));
await waitFor(() => {
expect(showErrorModal).toHaveBeenCalledWith(

View File

@@ -180,7 +180,7 @@ it('should close the modal when the onCancel event is triggered', async () => {
await waitFor(() => {
// check if the modal is not visible
const modal = document.getElementsByClassName('ant-modal');
const modal = document.querySelectorAll('.ant-modal');
const style = window.getComputedStyle(modal[0]);
expect(style.display).toBe('none');
});

View File

@@ -34,7 +34,7 @@ export const getViewDetailsUsingViewKey: GetViewDetailsUsingViewKey = (
const query = mapQueryDataFromApi(compositeQuery);
return { query, name, id, panelType: compositeQuery.panelType, extraData };
}
return undefined;
return;
};
export const omitIdFromQuery = (query: Query | null): any => ({
@@ -198,7 +198,7 @@ export const deleteViewHandler = ({
export const trimViewName = (viewName: string): string => {
if (viewName.length > 20) {
return `${viewName.substring(0, 20)}...`;
return `${viewName.slice(0, 20)}...`;
}
return viewName;
};

View File

@@ -9,7 +9,7 @@ const getOrCreateLegendList = (
id: string,
isLonger: boolean,
): HTMLUListElement => {
const legendContainer = document.getElementById(id);
const legendContainer = document.querySelector(`#${id}`);
let listContainer = legendContainer?.querySelector('ul');
if (!listContainer) {
@@ -26,7 +26,7 @@ const getOrCreateLegendList = (
listContainer.style.flexWrap = 'wrap';
listContainer.style.justifyContent = 'center';
listContainer.style.fontSize = '0.75rem';
legendContainer?.appendChild(listContainer);
legendContainer?.append(listContainer);
}
return listContainer;
@@ -64,7 +64,7 @@ export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
// li.style.marginTop = '5px';
li.onclick = (): void => {
// @ts-ignore
// @ts-expect-error
const { type } = chart.config;
if (type === 'pie' || type === 'doughnut') {
// Pie and doughnut charts only have a single dataset and visibility is per item
@@ -101,11 +101,11 @@ export const legend = (id: string, isLonger: boolean): Plugin<ChartType> => ({
textContainer.style.textDecoration = item.hidden ? 'line-through' : '';
const text = document.createTextNode(item.text);
textContainer.appendChild(text);
textContainer.append(text);
li.appendChild(boxSpan);
li.appendChild(textContainer);
ul.appendChild(li);
li.append(boxSpan);
li.append(textContainer);
ul.append(li);
}
});
},

View File

@@ -9,7 +9,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'millisecond');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
TIME_UNITS.millisecond,
);
}
@@ -17,7 +17,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'second');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
TIME_UNITS.second,
);
}
@@ -25,7 +25,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'minute');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
TIME_UNITS.minute,
);
}
@@ -33,7 +33,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'hour');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
TIME_UNITS.hour,
);
}
@@ -41,7 +41,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'day');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
TIME_UNITS.day,
);
}
@@ -49,7 +49,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'week');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
TIME_UNITS.week,
);
}
@@ -57,7 +57,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'month');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
TIME_UNITS.month,
);
}
@@ -65,7 +65,7 @@ describe('xAxisConfig for Chart', () => {
const start = dayjs();
const end = start.add(10, 'year');
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toEqual(
expect(convertTimeRange(start.valueOf(), end.valueOf()).unitName).toStrictEqual(
TIME_UNITS.year,
);
}

View File

@@ -7,7 +7,7 @@ const testFullPrecisionGetYAxisFormattedValue = (
): string => getYAxisFormattedValue(value, format, PrecisionOptionsEnum.FULL);
describe('getYAxisFormattedValue - none (full precision legacy assertions)', () => {
test('large integers and decimals', () => {
it('large integers and decimals', () => {
expect(testFullPrecisionGetYAxisFormattedValue('250034', 'none')).toBe(
'250034',
);
@@ -22,7 +22,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
);
});
test('preserves leading zeros after decimal until first non-zero', () => {
it('preserves leading zeros after decimal until first non-zero', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1.0000234', 'none')).toBe(
'1.0000234',
);
@@ -31,7 +31,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
);
});
test('trims to three significant decimals and removes trailing zeros', () => {
it('trims to three significant decimals and removes trailing zeros', () => {
expect(
testFullPrecisionGetYAxisFormattedValue('0.000000250034', 'none'),
).toBe('0.000000250034');
@@ -55,7 +55,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
).toBe('0.00000025');
});
test('whole numbers normalize', () => {
it('whole numbers normalize', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1000', 'none')).toBe('1000');
expect(testFullPrecisionGetYAxisFormattedValue('99.5458', 'none')).toBe(
'99.5458',
@@ -68,7 +68,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
);
});
test('strip redundant decimal zeros', () => {
it('strip redundant decimal zeros', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1000.000', 'none')).toBe(
'1000',
);
@@ -78,7 +78,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
expect(testFullPrecisionGetYAxisFormattedValue('1.000', 'none')).toBe('1');
});
test('edge values', () => {
it('edge values', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0', 'none')).toBe('0');
expect(testFullPrecisionGetYAxisFormattedValue('-0', 'none')).toBe('0');
expect(testFullPrecisionGetYAxisFormattedValue('Infinity', 'none')).toBe('∞');
@@ -92,7 +92,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
expect(testFullPrecisionGetYAxisFormattedValue('abc123', 'none')).toBe('NaN');
});
test('small decimals keep precision as-is', () => {
it('small decimals keep precision as-is', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.0001', 'none')).toBe(
'0.0001',
);
@@ -104,7 +104,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
);
});
test('simple decimals preserved', () => {
it('simple decimals preserved', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.1', 'none')).toBe('0.1');
expect(testFullPrecisionGetYAxisFormattedValue('0.2', 'none')).toBe('0.2');
expect(testFullPrecisionGetYAxisFormattedValue('0.3', 'none')).toBe('0.3');
@@ -115,7 +115,7 @@ describe('getYAxisFormattedValue - none (full precision legacy assertions)', ()
});
describe('getYAxisFormattedValue - units (full precision legacy assertions)', () => {
test('ms', () => {
it('ms', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1500', 'ms')).toBe('1.5 s');
expect(testFullPrecisionGetYAxisFormattedValue('500', 'ms')).toBe('500 ms');
expect(testFullPrecisionGetYAxisFormattedValue('60000', 'ms')).toBe('1 min');
@@ -127,19 +127,19 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
test('s', () => {
it('s', () => {
expect(testFullPrecisionGetYAxisFormattedValue('90', 's')).toBe('1.5 mins');
expect(testFullPrecisionGetYAxisFormattedValue('30', 's')).toBe('30 s');
expect(testFullPrecisionGetYAxisFormattedValue('3600', 's')).toBe('1 hour');
});
test('m', () => {
it('m', () => {
expect(testFullPrecisionGetYAxisFormattedValue('90', 'm')).toBe('1.5 hours');
expect(testFullPrecisionGetYAxisFormattedValue('30', 'm')).toBe('30 min');
expect(testFullPrecisionGetYAxisFormattedValue('1440', 'm')).toBe('1 day');
});
test('bytes', () => {
it('bytes', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1024', 'bytes')).toBe(
'1 KiB',
);
@@ -149,7 +149,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
test('mbytes', () => {
it('mbytes', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1024', 'mbytes')).toBe(
'1 GiB',
);
@@ -161,7 +161,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
test('kbytes', () => {
it('kbytes', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1024', 'kbytes')).toBe(
'1 MiB',
);
@@ -173,7 +173,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
test('short', () => {
it('short', () => {
expect(testFullPrecisionGetYAxisFormattedValue('1000', 'short')).toBe('1 K');
expect(testFullPrecisionGetYAxisFormattedValue('1500', 'short')).toBe(
'1.5 K',
@@ -201,7 +201,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
test('percent', () => {
it('percent', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.15', 'percent')).toBe(
'0.15%',
);
@@ -235,7 +235,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
).toBe('1.005555555595959%');
});
test('ratio', () => {
it('ratio', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0.5', 'ratio')).toBe(
'0.5 ratio',
);
@@ -247,7 +247,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
test('temperature units', () => {
it('temperature units', () => {
expect(testFullPrecisionGetYAxisFormattedValue('25', 'celsius')).toBe(
'25 °C',
);
@@ -267,13 +267,13 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
);
});
test('ms edge cases', () => {
it('ms edge cases', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0', 'ms')).toBe('0 ms');
expect(testFullPrecisionGetYAxisFormattedValue('-1500', 'ms')).toBe('-1.5 s');
expect(testFullPrecisionGetYAxisFormattedValue('Infinity', 'ms')).toBe('∞');
});
test('bytes edge cases', () => {
it('bytes edge cases', () => {
expect(testFullPrecisionGetYAxisFormattedValue('0', 'bytes')).toBe('0 B');
expect(testFullPrecisionGetYAxisFormattedValue('-1024', 'bytes')).toBe(
'-1 KiB',
@@ -282,7 +282,7 @@ describe('getYAxisFormattedValue - units (full precision legacy assertions)', ()
});
describe('getYAxisFormattedValue - precision option tests', () => {
test('precision 0 drops decimal part', () => {
it('precision 0 drops decimal part', () => {
expect(getYAxisFormattedValue('1.2345', 'none', 0)).toBe('1');
expect(getYAxisFormattedValue('0.9999', 'none', 0)).toBe('0');
expect(getYAxisFormattedValue('12345.6789', 'none', 0)).toBe('12345');
@@ -294,7 +294,7 @@ describe('getYAxisFormattedValue - precision option tests', () => {
// with unit
expect(getYAxisFormattedValue('4353.81', 'ms', 0)).toBe('4 s');
});
test('precision 1,2,3,4 decimals', () => {
it('precision 1,2,3,4 decimals', () => {
expect(getYAxisFormattedValue('1.2345', 'none', 1)).toBe('1.2');
expect(getYAxisFormattedValue('1.2345', 'none', 2)).toBe('1.23');
expect(getYAxisFormattedValue('1.2345', 'none', 3)).toBe('1.234');
@@ -345,7 +345,7 @@ describe('getYAxisFormattedValue - precision option tests', () => {
expect(getYAxisFormattedValue('0.123456', 'percent', 4)).toBe('0.1235%'); // approximation
});
test('precision full uses up to DEFAULT_SIGNIFICANT_DIGITS significant digits', () => {
it('precision full uses up to DEFAULT_SIGNIFICANT_DIGITS significant digits', () => {
expect(
getYAxisFormattedValue(
'0.00002625429914148441',

View File

@@ -109,8 +109,8 @@ export const useXAxisTimeUnit = (
};
const time = getTimeStamp(timeStamp as Date | number);
minTimeLocal = Math.min(parseInt(time.toString(), 10), minTimeLocal);
maxTimeLocal = Math.max(parseInt(time.toString(), 10), maxTimeLocal);
minTimeLocal = Math.min(Number.parseInt(time.toString(), 10), minTimeLocal);
maxTimeLocal = Math.max(Number.parseInt(time.toString(), 10), maxTimeLocal);
});
localTime = {

View File

@@ -25,7 +25,7 @@ export const getYAxisFormattedValue = (
format: string,
precision: PrecisionOption = 2, // default precision requested
): string => {
const numValue = parseFloat(value);
const numValue = Number.parseFloat(value);
// Handle non-numeric or special values first.
if (isNaN(numValue)) {
@@ -79,10 +79,10 @@ export const getYAxisFormattedValue = (
}
const formatter = getValueFormat(format);
const formattedValue = formatter(numValue, computeDecimals(), undefined);
const formattedValue = formatter(numValue, computeDecimals());
if (formattedValue.text && formattedValue.text.includes('.')) {
formattedValue.text = formatDecimalWithLeadingZeros(
parseFloat(formattedValue.text),
Number.parseFloat(formattedValue.text),
precision,
);
}

View File

@@ -284,7 +284,7 @@ describe('GuardAuthZ', () => {
expect(
screen.getAllByText(
new RegExp(permission.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
new RegExp(permission.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&')),
).length,
).toBeGreaterThan(0);
expect(screen.queryByText('Protected Content')).not.toBeInTheDocument();

View File

@@ -90,11 +90,9 @@ describe('InviteMembersModal', () => {
screen.getByRole('button', { name: /invite team members/i }),
);
expect(
await screen.findByText(
await expect(screen.findByText(
'Please enter valid emails and select roles for team members',
),
).toBeInTheDocument();
)).resolves.toBeInTheDocument();
});
it('shows email-only message when email is invalid but role is selected', async () => {
@@ -112,9 +110,7 @@ describe('InviteMembersModal', () => {
screen.getByRole('button', { name: /invite team members/i }),
);
expect(
await screen.findByText('Please enter valid emails for team members'),
).toBeInTheDocument();
await expect(screen.findByText('Please enter valid emails for team members')).resolves.toBeInTheDocument();
});
it('shows role-only message when email is valid but role is missing', async () => {
@@ -130,9 +126,7 @@ describe('InviteMembersModal', () => {
screen.getByRole('button', { name: /invite team members/i }),
);
expect(
await screen.findByText('Please select roles for team members'),
).toBeInTheDocument();
await expect(screen.findByText('Please select roles for team members')).resolves.toBeInTheDocument();
});
});
@@ -204,7 +198,7 @@ describe('InviteMembersModal', () => {
await user.type(emailInputs[1], 'bob@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
const editorOptions = await screen.findAllByText('Editor');
await user.click(editorOptions[editorOptions.length - 1]);
await user.click(editorOptions.at(-1));
await user.click(
screen.getByRole('button', { name: /invite team members/i }),
@@ -256,7 +250,7 @@ describe('InviteMembersModal', () => {
await user.type(emailInputs[1], 'bob@signoz.io');
await user.click(screen.getAllByText('Select roles')[0]);
const editorOptions = await screen.findAllByText('Editor');
await user.click(editorOptions[editorOptions.length - 1]);
await user.click(editorOptions.at(-1));
await user.click(
screen.getByRole('button', { name: /invite team members/i }),

View File

@@ -26,7 +26,7 @@ function QueryBuilderSearchWrapper({
const tagFiltersLength = tagFilters.items.length;
if (
(!tagFiltersLength && (!filters || !filters.items.length)) ||
(!tagFiltersLength && (!filters || filters.items.length === 0)) ||
tagFiltersLength === filters?.items.length ||
!contextQuery
) {

View File

@@ -163,7 +163,7 @@ function LogDetailInner({
}, [log.id, logs, onNavigateLog, onScrollToLog, selectedView]);
const listQuery = useMemo(() => {
if (!stagedQuery || stagedQuery.builder.queryData.length < 1) {
if (!stagedQuery || stagedQuery.builder.queryData.length === 0) {
return null;
}

View File

@@ -1,4 +1,6 @@
.log-state-indicator {
padding-left: 8px;
.line {
margin: 0 8px;
min-height: 24px;

View File

@@ -1,43 +0,0 @@
import { Color } from '@signozhq/design-tokens';
import { LogType } from './LogStateIndicator';
export function getRowBackgroundColor(
isDarkMode: boolean,
logType?: string,
): string {
if (isDarkMode) {
switch (logType) {
case LogType.INFO:
return `${Color.BG_ROBIN_500}40`;
case LogType.WARN:
return `${Color.BG_AMBER_500}40`;
case LogType.ERROR:
return `${Color.BG_CHERRY_500}40`;
case LogType.TRACE:
return `${Color.BG_FOREST_400}40`;
case LogType.DEBUG:
return `${Color.BG_AQUA_500}40`;
case LogType.FATAL:
return `${Color.BG_SAKURA_500}40`;
default:
return `${Color.BG_ROBIN_500}40`;
}
}
switch (logType) {
case LogType.INFO:
return Color.BG_ROBIN_100;
case LogType.WARN:
return Color.BG_AMBER_100;
case LogType.ERROR:
return Color.BG_CHERRY_100;
case LogType.TRACE:
return Color.BG_FOREST_200;
case LogType.DEBUG:
return Color.BG_AQUA_100;
case LogType.FATAL:
return Color.BG_SAKURA_100;
default:
return Color.BG_VANILLA_300;
}
}

View File

@@ -27,7 +27,7 @@ describe('getLogIndicatorType', () => {
expect(getLogIndicatorType(log)).toBe('TRACE');
});
it('severity_text should be used when severity_number is absent ', () => {
it('severity_text should be used when severity_number is absent', () => {
const log = {
date: '2024-02-29T12:34:46Z',
timestamp: 1646115296,
@@ -157,7 +157,7 @@ describe('logIndicatorBySeverityNumber', () => {
];
logLevelExpectations.forEach((e) => {
for (let sevNum = e.minSevNumber; sevNum <= e.maxSevNumber; sevNum++) {
const sevText = (Math.random() + 1).toString(36).substring(2);
const sevText = (Math.random() + 1).toString(36).slice(2);
const log = {
date: '2024-02-29T12:34:46Z',

View File

@@ -1,114 +0,0 @@
import type { ReactElement } from 'react';
import { useMemo } from 'react';
import TanStackTable from 'components/TanStackTableView';
import { DATE_TIME_FORMATS } from 'constants/dateTimeFormats';
import { getSanitizedLogBody } from 'container/LogDetailedView/utils';
import { FontSize } from 'container/OptionsMenu/types';
import { FlatLogData } from 'lib/logs/flatLogData';
import { useTimezone } from 'providers/Timezone';
import { IField } from 'types/api/logs/fields';
import { ILog } from 'types/api/logs/log';
import type { TableColumnDef } from '../../TanStackTableView/types';
import LogStateIndicator from '../LogStateIndicator/LogStateIndicator';
type UseLogsTableColumnsProps = {
fields: IField[];
fontSize: FontSize;
appendTo?: 'center' | 'end';
};
export function useLogsTableColumns({
fields,
fontSize,
appendTo = 'center',
}: UseLogsTableColumnsProps): TableColumnDef<ILog>[] {
const { formatTimezoneAdjustedTimestamp } = useTimezone();
return useMemo<TableColumnDef<ILog>[]>(() => {
const stateIndicatorCol: TableColumnDef<ILog> = {
id: 'state-indicator',
header: '',
pin: 'left',
enableMove: false,
enableResize: false,
enableRemove: false,
canBeHidden: false,
width: { fixed: 24 },
cell: ({ row }): ReactElement => (
<LogStateIndicator
fontSize={fontSize}
severityText={row.severity_text as string}
severityNumber={row.severity_number as number}
/>
),
};
const fieldColumns: TableColumnDef<ILog>[] = fields
.filter((f): boolean => !['id', 'body', 'timestamp'].includes(f.name))
.map(
(f): TableColumnDef<ILog> => ({
id: f.name,
header: f.name,
accessorFn: (log): unknown => FlatLogData(log)[f.name],
enableRemove: true,
width: { min: 192 },
cell: ({ value }): ReactElement => (
<TanStackTable.Text>{String(value ?? '')}</TanStackTable.Text>
),
}),
);
const timestampCol: TableColumnDef<ILog> | null = fields.some(
(f) => f.name === 'timestamp',
)
? {
id: 'timestamp',
header: 'Timestamp',
accessorFn: (log): unknown => log.timestamp,
width: { default: 170, min: 170 },
cell: ({ value }): ReactElement => {
const ts = value as string | number;
const formatted =
typeof ts === 'string'
? formatTimezoneAdjustedTimestamp(ts, DATE_TIME_FORMATS.ISO_DATETIME_MS)
: formatTimezoneAdjustedTimestamp(
ts / 1e6,
DATE_TIME_FORMATS.ISO_DATETIME_MS,
);
return <TanStackTable.Text>{formatted}</TanStackTable.Text>;
},
}
: null;
const bodyCol: TableColumnDef<ILog> | null = fields.some(
(f) => f.name === 'body',
)
? {
id: 'body',
header: 'Body',
accessorFn: (log): string => log.body,
canBeHidden: false,
width: { default: '100%', min: 300 },
cell: ({ value, isActive }): ReactElement => (
<TanStackTable.Text
dangerouslySetInnerHTML={{
__html: getSanitizedLogBody(value as string, {
shouldEscapeHtml: true,
}),
}}
data-active={isActive}
/>
),
}
: null;
return [
stateIndicatorCol,
...(timestampCol ? [timestampCol] : []),
...(appendTo === 'center' ? fieldColumns : []),
...(bodyCol ? [bodyCol] : []),
...(appendTo === 'end' ? fieldColumns : []),
];
}, [fields, appendTo, fontSize, formatTimezoneAdjustedTimestamp]);
}

View File

@@ -55,7 +55,7 @@ export const useTableView = (props: UseTableViewProps): UseTableViewResult => {
title: name,
dataIndex: name,
accessorKey: name,
id: name.toLowerCase().replace(/\./g, '_'),
id: name.toLowerCase().replaceAll(/\./g, '_'),
key: name,
render: (field): ColumnTypeRender<Record<string, unknown>> => ({
props: {

View File

@@ -77,7 +77,7 @@ function OptionsMenu({
};
const handleSearchValueChange = useDebouncedFn((event): void => {
// @ts-ignore
// @ts-expect-error
const value = event?.target?.value || '';
if (addColumn && addColumn?.onSearch) {

View File

@@ -72,7 +72,7 @@ describe('LogsFormatOptionsMenu (unit)', () => {
fireEvent.click(formatButton);
const getMenuItems = (): Element[] =>
Array.from(document.querySelectorAll('.menu-items .item'));
[...document.querySelectorAll('.menu-items .item')];
const findItemByLabel = (label: string): Element | undefined =>
getMenuItems().find((el) => (el.textContent || '').includes(label));
@@ -136,9 +136,7 @@ describe('LogsFormatOptionsMenu (unit)', () => {
fireEvent.click(fontButton);
// Choose MEDIUM
const optionButtons = Array.from(
document.querySelectorAll('.font-size-dropdown .option-btn'),
);
const optionButtons = [...document.querySelectorAll('.font-size-dropdown .option-btn')];
const mediumBtn = optionButtons[1] as HTMLElement;
fireEvent.click(mediumBtn);

View File

@@ -50,7 +50,7 @@ function Code({
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
// @ts-ignore
// @ts-expect-error
style={a11yDark}
language={match[1]}
PreTag="div"
@@ -115,7 +115,7 @@ function MarkdownRenderer({
className={className}
rehypePlugins={[rehypeRaw as any]}
components={{
// @ts-ignore
// @ts-expect-error
a: Link,
pre: ({ children }) =>
Pre({

View File

@@ -26,9 +26,9 @@ function MessagingQueueHealthCheck({
isFetching: consumerLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
enabled: serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Consumers,
).length,
).length > 0,
},
MessagingQueueHealthCheckService.Consumers,
);
@@ -40,9 +40,9 @@ function MessagingQueueHealthCheck({
isFetching: producerLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
enabled: serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Producers,
).length,
).length > 0,
},
MessagingQueueHealthCheckService.Producers,
);
@@ -54,9 +54,9 @@ function MessagingQueueHealthCheck({
isFetching: kafkaLoading,
} = useOnboardingStatus(
{
enabled: !!serviceToInclude.filter(
enabled: serviceToInclude.filter(
(service) => service === MessagingQueueHealthCheckService.Kafka,
).length,
).length > 0,
},
MessagingQueueHealthCheckService.Kafka,
);

View File

@@ -607,7 +607,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
try {
const parts = text.split(
new RegExp(
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
`(${searchQuery.replaceAll(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
'gi',
),
);
@@ -615,7 +615,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
<>
{parts.map((part, i) => {
// Create a unique key that doesn't rely on array index
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
const uniqueKey = `${text.slice(0, 3)}-${part.slice(0, 3)}-${i}`;
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">
@@ -819,7 +819,7 @@ const CustomMultiSelect: React.FC<CustomMultiSelectProps> = ({
const getLastVisibleChipIndex = useCallback((): number => {
const visibleIndices = getVisibleChipIndices();
return visibleIndices.length > 0
? visibleIndices[visibleIndices.length - 1]
? visibleIndices.at(-1)
: -1;
}, [getVisibleChipIndices]);

View File

@@ -152,7 +152,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
try {
const parts = text.split(
new RegExp(
`(${searchQuery.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
`(${searchQuery.replaceAll(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')})`,
'gi',
),
);
@@ -160,7 +160,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
<>
{parts.map((part, i) => {
// Create a deterministic but unique key
const uniqueKey = `${text.substring(0, 3)}-${part.substring(0, 3)}-${i}`;
const uniqueKey = `${text.slice(0, 3)}-${part.slice(0, 3)}-${i}`;
return part.toLowerCase() === searchQuery.toLowerCase() ? (
<span key={uniqueKey} className="highlight-text">

View File

@@ -61,7 +61,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 1. CUSTOM VALUES SUPPORT =====
describe('Custom Values Support (CS)', () => {
test('CS-01: Custom values persist in selected state', async () => {
it('CS-01: Custom values persist in selected state', async () => {
const { rerender } = renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -87,7 +87,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(screen.getByText('another-custom')).toBeInTheDocument();
});
test('CS-02: Partial matches create custom values', async () => {
it('CS-02: Partial matches create custom values', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -129,7 +129,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(combobox).toBeInTheDocument();
});
test('CS-03: Exact match filtering behavior', async () => {
it('CS-03: Exact match filtering behavior', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -154,14 +154,14 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted "frontend" text
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = Array.from(highlightedElements).map(
const highlightTexts = [...highlightedElements].map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('Frontend');
// Frontend option should be visible in dropdown - use a simpler approach
const optionLabels = document.querySelectorAll('.option-label-text');
const hasFrontendOption = Array.from(optionLabels).some((label) =>
const hasFrontendOption = [...optionLabels].some((label) =>
label.textContent?.includes('Frontend'),
);
expect(hasFrontendOption).toBe(true);
@@ -176,7 +176,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('CS-04: Search filtering with "end" pattern', async () => {
it('CS-04: Search filtering with "end" pattern', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -201,19 +201,19 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted "end" text in the options
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = Array.from(highlightedElements).map(
const highlightTexts = [...highlightedElements].map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('end');
// Check that Frontend and Backend options are present with highlighted text
const optionLabels = document.querySelectorAll('.option-label-text');
const hasFrontendOption = Array.from(optionLabels).some(
const hasFrontendOption = [...optionLabels].some(
(label) =>
label.textContent?.includes('Front') &&
label.textContent?.includes('end'),
);
const hasBackendOption = Array.from(optionLabels).some(
const hasBackendOption = [...optionLabels].some(
(label) =>
label.textContent?.includes('Back') && label.textContent?.includes('end'),
);
@@ -222,10 +222,10 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(hasBackendOption).toBe(true);
// Other options should be filtered out
const hasDatabaseOption = Array.from(optionLabels).some((label) =>
const hasDatabaseOption = [...optionLabels].some((label) =>
label.textContent?.includes('Database'),
);
const hasApiGatewayOption = Array.from(optionLabels).some((label) =>
const hasApiGatewayOption = [...optionLabels].some((label) =>
label.textContent?.includes('API Gateway'),
);
@@ -234,7 +234,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('CS-05: Comma-separated values behavior', async () => {
it('CS-05: Comma-separated values behavior', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -281,7 +281,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 2. SEARCH AND FILTERING =====
describe('Search and Filtering (SF)', () => {
test('SF-01: Selected values pushed to top', async () => {
it('SF-01: Selected values pushed to top', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -298,14 +298,14 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(dropdown).toBeInTheDocument();
const options = dropdown?.querySelectorAll('.option-label-text') || [];
const optionTexts = Array.from(options).map((el) => el.textContent);
const optionTexts = [...options].map((el) => el.textContent);
// Database should be at the top (after ALL option if present)
expect(optionTexts[0]).toBe('Database');
});
});
test('SF-02: Filtering with search text', async () => {
it('SF-02: Filtering with search text', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -332,14 +332,14 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted text within the Frontend option
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = Array.from(highlightedElements).map(
const highlightTexts = [...highlightedElements].map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('Front');
// Should show Frontend option (highlighted) - use a simpler approach
const optionLabels = document.querySelectorAll('.option-label-text');
const hasFrontendOption = Array.from(optionLabels).some((label) =>
const hasFrontendOption = [...optionLabels].some((label) =>
label.textContent?.includes('Frontend'),
);
expect(hasFrontendOption).toBe(true);
@@ -350,7 +350,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('SF-03: Highlighting search matches', async () => {
it('SF-03: Highlighting search matches', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -374,14 +374,14 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// Should highlight matching text in options
await waitFor(() => {
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = Array.from(highlightedElements).map(
const highlightTexts = [...highlightedElements].map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('end');
});
});
test('SF-04: Search with no results', async () => {
it('SF-04: Search with no results', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -424,7 +424,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 3. KEYBOARD NAVIGATION =====
describe('Keyboard Navigation (KN)', () => {
test('KN-01: Arrow key navigation in dropdown', async () => {
it('KN-01: Arrow key navigation in dropdown', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -465,7 +465,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('KN-02: Tab navigation to dropdown', async () => {
it('KN-02: Tab navigation to dropdown', async () => {
renderWithVirtuoso(
<div>
<input data-testid="prev-input" />
@@ -515,7 +515,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('KN-03: Enter selection in dropdown', async () => {
it('KN-03: Enter selection in dropdown', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -540,7 +540,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(mockOnChange).toHaveBeenCalledWith(['frontend'], ['frontend']);
});
test('KN-04: Chip deletion with keyboard', async () => {
it('KN-04: Chip deletion with keyboard', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -586,7 +586,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 5. UI/UX BEHAVIORS =====
describe('UI/UX Behaviors (UI)', () => {
test('UI-01: Loading state does not block interaction', async () => {
it('UI-01: Loading state does not block interaction', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -603,7 +603,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('UI-02: Component remains editable in all states', async () => {
it('UI-02: Component remains editable in all states', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -634,7 +634,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(combobox).not.toBeDisabled();
});
test('UI-03: Toggle/Only labels in dropdown', async () => {
it('UI-03: Toggle/Only labels in dropdown', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -656,7 +656,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('UI-04: Should display values with loading info at bottom', async () => {
it('UI-04: Should display values with loading info at bottom', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -677,7 +677,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('UI-05: Error state display in footer', async () => {
it('UI-05: Error state display in footer', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -696,7 +696,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('UI-06: No data state display', async () => {
it('UI-06: No data state display', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={[]}
@@ -716,7 +716,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 6. CLEAR ACTIONS =====
describe('Clear Actions (CA)', () => {
test('CA-01: Ctrl+A selects all chips', async () => {
it('CA-01: Ctrl+A selects all chips', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -760,7 +760,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('CA-02: Clear icon removes all selections', async () => {
it('CA-02: Clear icon removes all selections', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -777,7 +777,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
}
});
test('CA-03: Individual chip removal', async () => {
it('CA-03: Individual chip removal', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -790,7 +790,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
const removeButtons = document.querySelectorAll(
'.ant-select-selection-item-remove',
);
expect(removeButtons.length).toBe(2);
expect(removeButtons).toHaveLength(2);
await user.click(removeButtons[1] as Element);
@@ -804,7 +804,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 7. SAVE AND SELECTION TRIGGERS =====
describe('Save and Selection Triggers (ST)', () => {
test('ST-01: ESC triggers save action', async () => {
it('ST-01: ESC triggers save action', async () => {
const mockDropdownChange = jest.fn();
renderWithVirtuoso(
@@ -837,7 +837,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('ST-02: Mouse selection works', async () => {
it('ST-02: Mouse selection works', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -859,7 +859,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
);
});
test('ST-03: ENTER in input field creates custom value', async () => {
it('ST-03: ENTER in input field creates custom value', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -892,7 +892,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('ST-04: Search text persistence', async () => {
it('ST-04: Search text persistence', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -932,7 +932,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 8. SPECIAL OPTIONS AND STATES =====
describe('Special Options and States (SO)', () => {
test('SO-01: ALL option appears first and separated', async () => {
it('SO-01: ALL option appears first and separated', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -954,7 +954,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('SO-02: ALL selection behavior', async () => {
it('SO-02: ALL selection behavior', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -981,7 +981,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
);
});
test('SO-03: ALL tag display when all selected', () => {
it('SO-03: ALL tag display when all selected', () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -996,7 +996,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(screen.queryByText('frontend')).not.toBeInTheDocument();
});
test('SO-04: Footer information display', async () => {
it('SO-04: Footer information display', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1017,7 +1017,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== GROUPED OPTIONS SUPPORT =====
describe('Grouped Options Support', () => {
test('handles grouped options correctly', async () => {
it('handles grouped options correctly', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockGroupedOptions} onChange={mockOnChange} />,
);
@@ -1041,7 +1041,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== ACCESSIBILITY TESTS =====
describe('Accessibility', () => {
test('has proper ARIA attributes', async () => {
it('has proper ARIA attributes', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1058,7 +1058,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('supports screen reader navigation', async () => {
it('supports screen reader navigation', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1079,7 +1079,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 9. ADVANCED KEYBOARD NAVIGATION =====
describe('Advanced Keyboard Navigation (AKN)', () => {
test('AKN-01: Shift + Arrow + Del chip deletion', async () => {
it('AKN-01: Shift + Arrow + Del chip deletion', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -1137,7 +1137,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(combobox).toHaveFocus();
});
test('AKN-03: Mouse out closes dropdown', async () => {
it('AKN-03: Mouse out closes dropdown', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1164,7 +1164,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 10. ADVANCED FILTERING AND HIGHLIGHTING =====
describe('Advanced Filtering and Highlighting (AFH)', () => {
test('AFH-01: Highlighted values pushed to top', async () => {
it('AFH-01: Highlighted values pushed to top', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1189,14 +1189,14 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted text
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = Array.from(highlightedElements).map(
const highlightTexts = [...highlightedElements].map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('front');
// Get all option items to check the order
const optionItems = document.querySelectorAll('.option-item');
const optionTexts = Array.from(optionItems)
const optionTexts = [...optionItems]
.map((item) => {
const labelElement = item.querySelector('.option-label-text');
return labelElement?.textContent?.trim();
@@ -1220,7 +1220,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('AFH-02: Distinction between selection Enter and save Enter', async () => {
it('AFH-02: Distinction between selection Enter and save Enter', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1267,7 +1267,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 11. ADVANCED CLEAR ACTIONS =====
describe('Advanced Clear Actions (ACA)', () => {
test('ACA-01: Clear action waiting behavior', async () => {
it('ACA-01: Clear action waiting behavior', async () => {
const mockOnChangeWithDelay = jest.fn().mockImplementation(
() =>
new Promise<void>((resolve) => {
@@ -1300,7 +1300,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 12. ADVANCED UI STATES =====
describe('Advanced UI States (AUS)', () => {
test('AUS-01: No data with previous value selected', async () => {
it('AUS-01: No data with previous value selected', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={[]}
@@ -1322,7 +1322,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(screen.getByText('previous-value')).toBeInTheDocument();
});
test('AUS-02: Always editable accessibility', async () => {
it('AUS-02: Always editable accessibility', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -1338,7 +1338,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
expect(combobox).not.toBeDisabled();
});
test('AUS-03: Sufficient space for search value', async () => {
it('AUS-03: Sufficient space for search value', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);
@@ -1372,7 +1372,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 13. REGEX AND CUSTOM VALUES =====
describe('Regex and Custom Values (RCV)', () => {
test('RCV-01: Regex pattern support', async () => {
it('RCV-01: Regex pattern support', async () => {
renderWithVirtuoso(
<CustomMultiSelect
options={mockOptions}
@@ -1418,7 +1418,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
});
});
test('RCV-02: Custom values treated as normal dropdown values', async () => {
it('RCV-02: Custom values treated as normal dropdown values', async () => {
const customOptions = [
...mockOptions,
{ label: 'custom-value', value: 'custom-value', type: 'custom' as const },
@@ -1456,7 +1456,7 @@ describe('CustomMultiSelect - Comprehensive Tests', () => {
// ===== 14. DROPDOWN PERSISTENCE =====
describe('Dropdown Persistence (DP)', () => {
test('DP-01: Dropdown stays open for non-save actions', async () => {
it('DP-01: Dropdown stays open for non-save actions', async () => {
renderWithVirtuoso(
<CustomMultiSelect options={mockOptions} onChange={mockOnChange} />,
);

View File

@@ -50,7 +50,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 1. CUSTOM VALUES SUPPORT =====
describe('Custom Values Support (CS)', () => {
test('CS-02: Partial matches create custom values', async () => {
it('CS-02: Partial matches create custom values', async () => {
render(
<CustomSelect
options={mockOptions}
@@ -110,7 +110,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('CS-03: Exact match behavior', async () => {
it('CS-03: Exact match behavior', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -133,14 +133,14 @@ describe('CustomSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted "frontend" text
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = Array.from(highlightedElements).map(
const highlightTexts = [...highlightedElements].map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('Frontend');
// Frontend option should be visible in dropdown - use a simpler approach
const optionContents = document.querySelectorAll('.option-content');
const hasFrontendOption = Array.from(optionContents).some((content) =>
const hasFrontendOption = [...optionContents].some((content) =>
content.textContent?.includes('Frontend'),
);
expect(hasFrontendOption).toBe(true);
@@ -161,7 +161,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 2. SEARCH AND FILTERING =====
describe('Search and Filtering (SF)', () => {
test('SF-01: Selected values pushed to top', async () => {
it('SF-01: Selected values pushed to top', async () => {
render(
<CustomSelect
options={mockOptions}
@@ -178,14 +178,14 @@ describe('CustomSelect - Comprehensive Tests', () => {
expect(dropdown).toBeInTheDocument();
const options = dropdown?.querySelectorAll('.option-content') || [];
const optionTexts = Array.from(options).map((el) => el.textContent);
const optionTexts = [...options].map((el) => el.textContent);
// Database should be at the top
expect(optionTexts[0]).toContain('Database');
});
});
test('SF-02: Real-time search filtering', async () => {
it('SF-02: Real-time search filtering', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -210,14 +210,14 @@ describe('CustomSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted text within the Frontend option
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = Array.from(highlightedElements).map(
const highlightTexts = [...highlightedElements].map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('Front');
// Should show Frontend option (highlighted) - use a simpler approach
const optionContents = document.querySelectorAll('.option-content');
const hasFrontendOption = Array.from(optionContents).some((content) =>
const hasFrontendOption = [...optionContents].some((content) =>
content.textContent?.includes('Frontend'),
);
expect(hasFrontendOption).toBe(true);
@@ -228,7 +228,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('SF-03: Search highlighting', async () => {
it('SF-03: Search highlighting', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -250,14 +250,14 @@ describe('CustomSelect - Comprehensive Tests', () => {
// Should highlight matching text in options
await waitFor(() => {
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = Array.from(highlightedElements).map(
const highlightTexts = [...highlightedElements].map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('end');
});
});
test('SF-04: Search with partial matches', async () => {
it('SF-04: Search with partial matches', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -298,7 +298,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 3. KEYBOARD NAVIGATION =====
describe('Keyboard Navigation (KN)', () => {
test('KN-01: Arrow key navigation in dropdown', async () => {
it('KN-01: Arrow key navigation in dropdown', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -329,7 +329,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('KN-02: Tab navigation to dropdown', async () => {
it('KN-02: Tab navigation to dropdown', async () => {
render(
<div>
<input data-testid="prev-input" />
@@ -355,7 +355,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('KN-03: Enter selection in dropdown', async () => {
it('KN-03: Enter selection in dropdown', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -376,7 +376,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('KN-04: Space key selection', async () => {
it('KN-04: Space key selection', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -396,7 +396,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('KN-05: Tab navigation within dropdown', async () => {
it('KN-05: Tab navigation within dropdown', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -417,7 +417,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 4. UI/UX BEHAVIORS =====
describe('UI/UX Behaviors (UI)', () => {
test('UI-01: Loading state does not block interaction', async () => {
it('UI-01: Loading state does not block interaction', async () => {
render(
<CustomSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -429,7 +429,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
expect(combobox).toHaveFocus();
});
test('UI-02: Component remains editable in all states', () => {
it('UI-02: Component remains editable in all states', () => {
render(
<CustomSelect
options={mockOptions}
@@ -444,7 +444,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
expect(combobox).not.toBeDisabled();
});
test('UI-03: Loading state display in footer', async () => {
it('UI-03: Loading state display in footer', async () => {
render(
<CustomSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -458,7 +458,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('UI-04: Error state display in footer', async () => {
it('UI-04: Error state display in footer', async () => {
render(
<CustomSelect
options={mockOptions}
@@ -477,7 +477,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('UI-05: No data state display', async () => {
it('UI-05: No data state display', async () => {
render(
<CustomSelect
options={[]}
@@ -497,7 +497,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 6. SAVE AND SELECTION TRIGGERS =====
describe('Save and Selection Triggers (ST)', () => {
test('ST-01: Mouse selection works', async () => {
it('ST-01: Mouse selection works', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -520,7 +520,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 7. GROUPED OPTIONS SUPPORT =====
describe('Grouped Options Support', () => {
test('handles grouped options correctly', async () => {
it('handles grouped options correctly', async () => {
render(
<CustomSelect options={mockGroupedOptions} onChange={mockOnChange} />,
);
@@ -541,7 +541,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('grouped option selection works', async () => {
it('grouped option selection works', async () => {
render(
<CustomSelect options={mockGroupedOptions} onChange={mockOnChange} />,
);
@@ -566,7 +566,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 8. ACCESSIBILITY =====
describe('Accessibility', () => {
test('has proper ARIA attributes', async () => {
it('has proper ARIA attributes', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -580,7 +580,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('supports screen reader navigation', async () => {
it('supports screen reader navigation', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -596,7 +596,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('has proper focus management', async () => {
it('has proper focus management', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -617,7 +617,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 10. EDGE CASES =====
describe('Edge Cases', () => {
test('handles special characters in options', async () => {
it('handles special characters in options', async () => {
const specialOptions = [
{ label: 'Option with spaces', value: 'option-with-spaces' },
{ label: 'Option-with-dashes', value: 'option-with-dashes' },
@@ -638,7 +638,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('handles extremely long option labels', async () => {
it('handles extremely long option labels', async () => {
const longLabelOptions = [
{
label:
@@ -663,7 +663,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 11. ADVANCED KEYBOARD NAVIGATION =====
describe('Advanced Keyboard Navigation (AKN)', () => {
test('AKN-01: Mouse out closes dropdown', async () => {
it('AKN-01: Mouse out closes dropdown', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -684,7 +684,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('AKN-02: TAB navigation from input to dropdown', async () => {
it('AKN-02: TAB navigation from input to dropdown', async () => {
render(
<div>
<input data-testid="prev-input" />
@@ -722,7 +722,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 12. ADVANCED FILTERING AND HIGHLIGHTING =====
describe('Advanced Filtering and Highlighting (AFH)', () => {
test('AFH-01: Highlighted values pushed to top', async () => {
it('AFH-01: Highlighted values pushed to top', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -745,14 +745,14 @@ describe('CustomSelect - Comprehensive Tests', () => {
await waitFor(() => {
// Check for highlighted text
const highlightedElements = document.querySelectorAll('.highlight-text');
const highlightTexts = Array.from(highlightedElements).map(
const highlightTexts = [...highlightedElements].map(
(el) => el.textContent,
);
expect(highlightTexts).toContain('front');
// Get all option items to check the order
const optionItems = document.querySelectorAll('.option-item');
const optionTexts = Array.from(optionItems)
const optionTexts = [...optionItems]
.map((item) => {
const contentElement = item.querySelector('.option-content');
return contentElement?.textContent?.trim();
@@ -776,7 +776,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('AFH-02: Distinction between selection Enter and save Enter', async () => {
it('AFH-02: Distinction between selection Enter and save Enter', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -830,7 +830,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 13. ADVANCED CLEAR ACTIONS =====
describe('Advanced Clear Actions (ACA)', () => {
test('ACA-01: Clear action waiting behavior', async () => {
it('ACA-01: Clear action waiting behavior', async () => {
const mockOnChangeWithDelay = jest.fn().mockImplementation(
() =>
new Promise((resolve) => {
@@ -860,7 +860,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
expect(mockOnChangeWithDelay).toHaveBeenCalled();
});
test('ACA-02: Single select clear behavior like text input', async () => {
it('ACA-02: Single select clear behavior like text input', async () => {
render(
<CustomSelect
options={mockOptions}
@@ -883,7 +883,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 14. ADVANCED UI STATES =====
describe('Advanced UI States (AUS)', () => {
test('AUS-01: No data with previous value selected', async () => {
it('AUS-01: No data with previous value selected', async () => {
render(
<CustomSelect
options={[]}
@@ -905,7 +905,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
expect(screen.getAllByText('previous-value')).toHaveLength(2);
});
test('AUS-02: Always editable accessibility', async () => {
it('AUS-02: Always editable accessibility', async () => {
render(
<CustomSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -921,7 +921,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
expect(combobox).not.toBeDisabled();
});
test('AUS-03: Sufficient space for search value', async () => {
it('AUS-03: Sufficient space for search value', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -950,7 +950,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('AUS-04: No spinners blocking user interaction', async () => {
it('AUS-04: No spinners blocking user interaction', async () => {
render(
<CustomSelect options={mockOptions} onChange={mockOnChange} loading />,
);
@@ -976,7 +976,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 15. REGEX AND CUSTOM VALUES =====
describe('Regex and Custom Values (RCV)', () => {
test('RCV-01: Regex pattern support', async () => {
it('RCV-01: Regex pattern support', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');
@@ -1019,7 +1019,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
});
});
test('RCV-02: Custom values treated as normal dropdown values', async () => {
it('RCV-02: Custom values treated as normal dropdown values', async () => {
const customOptions = [
...mockOptions,
{ label: 'custom-value', value: 'custom-value', type: 'custom' as const },
@@ -1051,7 +1051,7 @@ describe('CustomSelect - Comprehensive Tests', () => {
// ===== 16. DROPDOWN PERSISTENCE =====
describe('Dropdown Persistence (DP)', () => {
test('DP-01: Dropdown closes only on save actions', async () => {
it('DP-01: Dropdown closes only on save actions', async () => {
render(<CustomSelect options={mockOptions} onChange={mockOnChange} />);
const combobox = screen.getByRole('combobox');

View File

@@ -86,7 +86,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 1. INTEGRATION WITH CUSTOMSELECT =====
describe('CustomSelect Integration (VI)', () => {
test('VI-01: Single select variable integration', async () => {
it('VI-01: Single select variable integration', async () => {
const variable = createMockVariable({
multiSelect: false,
type: 'CUSTOM',
@@ -130,7 +130,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 2. INTEGRATION WITH CUSTOMMULTISELECT =====
describe('CustomMultiSelect Integration (VI)', () => {
test('VI-02: Multi select variable integration', async () => {
it('VI-02: Multi select variable integration', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
@@ -174,7 +174,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 3. TEXTBOX VARIABLE TYPE =====
describe('Textbox Variable Integration', () => {
test('VI-03: Textbox variable handling', async () => {
it('VI-03: Textbox variable handling', async () => {
const variable = createMockVariable({
type: 'TEXTBOX',
selectedValue: 'initial-value',
@@ -219,7 +219,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 4. VALUE PERSISTENCE AND STATE MANAGEMENT =====
describe('Value Persistence and State Management', () => {
test('VI-04: All selected state handling', () => {
it('VI-04: All selected state handling', () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
@@ -243,7 +243,7 @@ describe('VariableItem Integration Tests', () => {
expect(screen.getByText('ALL')).toBeInTheDocument();
});
test('VI-05: Dropdown behavior with temporary selections', async () => {
it('VI-05: Dropdown behavior with temporary selections', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
@@ -277,7 +277,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 6. ACCESSIBILITY AND USER EXPERIENCE =====
describe('Accessibility and User Experience', () => {
test('VI-06: Variable description tooltip', async () => {
it('VI-06: Variable description tooltip', async () => {
const variable = createMockVariable({
description: 'This variable controls the service selection',
type: 'CUSTOM',
@@ -310,7 +310,7 @@ describe('VariableItem Integration Tests', () => {
});
});
test('VI-07: Variable name display', () => {
it('VI-07: Variable name display', () => {
const variable = createMockVariable({
name: 'service_name',
type: 'CUSTOM',
@@ -331,7 +331,7 @@ describe('VariableItem Integration Tests', () => {
expect(screen.getByText('$service_name')).toBeInTheDocument();
});
test('VI-08: Max tag count behavior', async () => {
it('VI-08: Max tag count behavior', async () => {
const variable = createMockVariable({
multiSelect: true,
type: 'CUSTOM',
@@ -365,7 +365,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 8. SEARCH INTERACTION TESTS =====
describe('Search Interaction Tests', () => {
test('VI-14: Search persistence across dropdown open/close', async () => {
it('VI-14: Search persistence across dropdown open/close', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',
@@ -417,7 +417,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 9. ADVANCED KEYBOARD NAVIGATION =====
describe('Advanced Keyboard Navigation (VI)', () => {
test('VI-15: Shift + Arrow + Del chip deletion in multiselect', async () => {
it('VI-15: Shift + Arrow + Del chip deletion in multiselect', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',
@@ -461,7 +461,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 11. ADVANCED UI STATES =====
describe('Advanced UI States (VI)', () => {
test('VI-19: No data with previous value selected in variable', async () => {
it('VI-19: No data with previous value selected in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: '',
@@ -499,7 +499,7 @@ describe('VariableItem Integration Tests', () => {
expect(combobox).toBeInTheDocument();
});
test('VI-20: Always editable accessibility in variable', async () => {
it('VI-20: Always editable accessibility in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2',
@@ -530,7 +530,7 @@ describe('VariableItem Integration Tests', () => {
// ===== 13. DROPDOWN PERSISTENCE =====
describe('Dropdown Persistence (VI)', () => {
test('VI-24: Dropdown stays open for non-save actions in variable', async () => {
it('VI-24: Dropdown stays open for non-save actions in variable', async () => {
const variable = createMockVariable({
type: 'CUSTOM',
customValue: 'option1,option2,option3',

View File

@@ -44,7 +44,7 @@ describe('OverflowInputToolTip', () => {
jest.restoreAllMocks();
});
test('shows tooltip when content overflows and input is clamped at maxAutoWidth', async () => {
it('shows tooltip when content overflows and input is clamped at maxAutoWidth', async () => {
mockOverflow(150, 250); // clientWidth >= maxAutoWidth (150), scrollWidth > clientWidth
render(<OverflowInputToolTip value="Very long overflowing text" />);
@@ -64,7 +64,7 @@ describe('OverflowInputToolTip', () => {
).toBeInTheDocument();
});
test('does NOT show tooltip when content does not overflow', async () => {
it('does NOT show tooltip when content does not overflow', async () => {
mockOverflow(150, 100); // content fits (scrollWidth <= clientWidth)
render(<OverflowInputToolTip value="Short text" />);
@@ -76,7 +76,7 @@ describe('OverflowInputToolTip', () => {
});
});
test('does NOT show tooltip when content overflows but input is NOT at maxAutoWidth', async () => {
it('does NOT show tooltip when content overflows but input is NOT at maxAutoWidth', async () => {
mockOverflow(100, 250); // clientWidth < maxAutoWidth (150), scrollWidth > clientWidth
render(<OverflowInputToolTip value="Long but input not clamped" />);
@@ -88,7 +88,7 @@ describe('OverflowInputToolTip', () => {
});
});
test('uncontrolled input allows typing', async () => {
it('uncontrolled input allows typing', async () => {
render(<OverflowInputToolTip defaultValue="Init" />);
const input = screen.getByRole('textbox') as HTMLInputElement;
@@ -97,7 +97,7 @@ describe('OverflowInputToolTip', () => {
expect(input).toHaveValue('InitABC');
});
test('disabled input never shows tooltip even if overflowing', async () => {
it('disabled input never shows tooltip even if overflowing', async () => {
mockOverflow(150, 300);
render(<OverflowInputToolTip value="Overflowing!" disabled />);
@@ -109,7 +109,7 @@ describe('OverflowInputToolTip', () => {
});
});
test('renders mirror span and input correctly (structural assertions instead of snapshot)', () => {
it('renders mirror span and input correctly (structural assertions instead of snapshot)', () => {
const { container } = render(<OverflowInputToolTip value="Snapshot" />);
const mirror = container.querySelector('.overflow-input-mirror');
const input = container.querySelector('input') as HTMLInputElement | null;

View File

@@ -25,7 +25,7 @@ function OverlayScrollbar({
autoHide: 'scroll',
theme: isDarkMode ? 'os-theme-light' : 'os-theme-dark',
},
...(customOptions || {}),
...customOptions,
} as PartialOptions),
[customOptions, isDarkMode],
);

View File

@@ -159,7 +159,7 @@ function HavingFilter({
if (tokens.length === 0) {
return false;
}
const lastToken = tokens[tokens.length - 1];
const lastToken = tokens.at(-1);
// Check if the last token is exactly an operator or ends with an operator and space
return havingOperators.some((op) => {
const opWithSpace = `${op.value} `;
@@ -253,7 +253,7 @@ function HavingFilter({
if (
!text.endsWith(' ') &&
tokens.length >= 2 &&
havingOperators.some((op) => op.value === tokens[tokens.length - 2])
havingOperators.some((op) => op.value === tokens.at(-2))
) {
return null;
}
@@ -261,8 +261,8 @@ function HavingFilter({
// Suggest key/operator pairs and ( for grouping
if (
tokens.length === 0 ||
conjunctions.some((c) => tokens[tokens.length - 1] === c.value.trim()) ||
tokens[tokens.length - 1] === '('
conjunctions.some((c) => tokens.at(-1) === c.value.trim()) ||
tokens.at(-1) === '('
) {
return {
from: context.pos,
@@ -275,7 +275,7 @@ function HavingFilter({
// Show suggestions when typing
if (tokens.length > 0) {
const lastToken = tokens[tokens.length - 1];
const lastToken = tokens.at(-1);
const filteredOptions = options.filter((opt) =>
opt.label.toLowerCase().includes(lastToken.toLowerCase()),
);
@@ -293,8 +293,8 @@ function HavingFilter({
// Suggest conjunctions after a value and a space
if (
tokens.length > 0 &&
(isNumber(tokens[tokens.length - 1]) ||
tokens[tokens.length - 1] === ')') &&
(isNumber(tokens.at(-1)) ||
tokens.at(-1) === ')') &&
text.endsWith(' ')
) {
return {

View File

@@ -535,7 +535,7 @@ function QueryAddOns({
>
<Radio.Button
className={
selectedViews.find((view) => view.key === addOn.key)
selectedViews.some((view) => view.key === addOn.key)
? 'selected-view tab'
: 'tab'
}

View File

@@ -23,7 +23,7 @@ function QueryAggregationOptions({
panelType?: string;
onAggregationIntervalChange: (value: number) => void;
onChange?: (value: string) => void;
queryData: IBuilderQuery | IBuilderTraceOperator;
queryData: IBuilderQuery ;
}): JSX.Element {
const showAggregationInterval = useMemo(() => {
if (panelType === PANEL_TYPES.VALUE) {

View File

@@ -286,7 +286,7 @@ function QuerySearch({
}
});
}
setKeySuggestions(Array.from(merged.values()));
setKeySuggestions([...merged.values()]);
// Force reopen the completion if editor is available and focused
if (editorRef.current) {
@@ -339,7 +339,7 @@ function QuerySearch({
// If value contains single quotes, escape them and wrap in single quotes
if (value.includes("'")) {
// Replace single quotes with escaped single quotes
const escapedValue = value.replace(/'/g, "\\'");
const escapedValue = value.replaceAll(/'/g, "\\'");
return `'${escapedValue}'`;
}
@@ -899,12 +899,12 @@ function QuerySearch({
// If we have previous pairs, we can prioritize keys that haven't been used yet
if (queryContext.queryPairs && queryContext.queryPairs.length > 0) {
const usedKeys = queryContext.queryPairs.map((pair) => pair.key);
const usedKeys = new Set(queryContext.queryPairs.map((pair) => pair.key));
// Add boost to unused keys to prioritize them
options = options.map((option) => ({
...option,
boost: usedKeys.includes(option.label) ? -10 : 10,
boost: usedKeys.has(option.label) ? -10 : 10,
}));
}

View File

@@ -29,7 +29,7 @@ describe('traceOperatorContextUtils', () => {
null,
);
expect(context).toEqual({
expect(context).toStrictEqual({
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
@@ -62,7 +62,7 @@ describe('traceOperatorContextUtils', () => {
false,
);
expect(context).toEqual({
expect(context).toStrictEqual({
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
@@ -193,7 +193,7 @@ describe('traceOperatorContextUtils', () => {
it('should return default context for empty query', () => {
const result = getTraceOperatorContextAtCursor('', 0);
expect(result).toEqual({
expect(result).toStrictEqual({
tokenType: -1,
text: '',
start: 0,
@@ -211,7 +211,7 @@ describe('traceOperatorContextUtils', () => {
it('should return default context for null query', () => {
const result = getTraceOperatorContextAtCursor(null as any, 0);
expect(result).toEqual({
expect(result).toStrictEqual({
tokenType: -1,
text: '',
start: 0,
@@ -229,7 +229,7 @@ describe('traceOperatorContextUtils', () => {
it('should return default context for undefined query', () => {
const result = getTraceOperatorContextAtCursor(undefined as any, 0);
expect(result).toEqual({
expect(result).toStrictEqual({
tokenType: -1,
text: '',
start: 0,

View File

@@ -8,21 +8,21 @@ const makeTraceOperator = (expression: string): IBuilderTraceOperator =>
describe('getInvolvedQueriesInTraceOperator', () => {
it('returns empty array for empty input', () => {
const result = getInvolvedQueriesInTraceOperator([]);
expect(result).toEqual([]);
expect(result).toStrictEqual([]);
});
it('extracts identifiers from expression', () => {
const result = getInvolvedQueriesInTraceOperator([
makeTraceOperator('A => B'),
]);
expect(result).toEqual(['A', 'B']);
expect(result).toStrictEqual(['A', 'B']);
});
it('extracts identifiers from complex expression', () => {
const result = getInvolvedQueriesInTraceOperator([
makeTraceOperator('A => (NOT B || C)'),
]);
expect(result).toEqual(['A', 'B', 'C']);
expect(result).toStrictEqual(['A', 'B', 'C']);
});
it('filters out querynames from complex expression', () => {
@@ -31,7 +31,7 @@ describe('getInvolvedQueriesInTraceOperator', () => {
'(A1 && (NOT B2 || (C3 -> (D4 && E5)))) => ((F6 || G7) && (NOT (H8 -> I9)))',
),
]);
expect(result).toEqual([
expect(result).toStrictEqual([
'A1',
'B2',
'C3',

View File

@@ -222,9 +222,7 @@ describe('QuerySearch (Integration with Real CodeMirror)', () => {
timeout: 2000,
});
const lastArgs = mockedGetKeysOnMount.mock.calls[
mockedGetKeysOnMount.mock.calls.length - 1
]?.[0] as { signal: unknown; searchText: string };
const lastArgs = mockedGetKeysOnMount.mock.calls.at(-1)?.[0] as { signal: unknown; searchText: string };
expect(lastArgs).toMatchObject({ signal: DataSource.LOGS, searchText: '' });
});

View File

@@ -85,7 +85,7 @@ describe('previousQuery.utils', () => {
saveAsPreviousQuery(key, sampleQuery);
const fromStore = getPreviousQueryFromKey(key);
expect(fromStore).toEqual(sampleQuery);
expect(fromStore).toStrictEqual(sampleQuery);
});
it('saveAsPreviousQuery merges multiple entries and removeKeyFromPreviousQuery deletes one', () => {

View File

@@ -22,18 +22,18 @@ describe('convertFiltersToExpression', () => {
it('should handle empty, null, and undefined inputs', () => {
// Test null and undefined
expect(convertFiltersToExpression(null as any)).toEqual({ expression: '' });
expect(convertFiltersToExpression(undefined as any)).toEqual({
expect(convertFiltersToExpression(null as any)).toStrictEqual({ expression: '' });
expect(convertFiltersToExpression(undefined as any)).toStrictEqual({
expression: '',
});
// Test empty filters
expect(convertFiltersToExpression({ items: [], op: 'AND' })).toEqual({
expect(convertFiltersToExpression({ items: [], op: 'AND' })).toStrictEqual({
expression: '',
});
expect(
convertFiltersToExpression({ items: undefined, op: 'AND' } as any),
).toEqual({ expression: '' });
).toStrictEqual({ expression: '' });
});
it('should convert basic comparison operators with proper value formatting', () => {
@@ -92,7 +92,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expect(result).toStrictEqual({
expression:
"service = 'api-gateway' AND status != 'error' AND duration > 100 AND count <= 50 AND is_active = true AND enabled = false AND count = 0 AND regex REGEXP '.*'",
});
@@ -124,7 +124,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expect(result).toStrictEqual({
expression:
"message = 'user\\'s data' AND description = '' AND path = '/api/v1/users'",
});
@@ -162,7 +162,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expect(result).toStrictEqual({
expression:
"service in ['api-gateway', 'user-service', 'auth-service'] AND status in ['success'] AND tags in [] AND name in ['John\\'s', 'Mary\\'s', 'Bob']",
});
@@ -224,7 +224,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expect(result).toStrictEqual({
expression:
"service NOT IN ['api-gateway', 'user-service'] AND message NOT LIKE 'error' AND path NOT REGEXP '/api/.*' AND service NOT IN ['api-gateway'] AND user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
});
@@ -268,7 +268,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expect(result).toStrictEqual({
expression:
"user_id exists AND user_id exists AND has(tags, 'production') AND hasAny(tags, ['production', 'staging']) AND hasAll(tags, ['production', 'monitoring'])",
});
@@ -312,7 +312,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expect(result).toStrictEqual({
expression:
"service = 'api-gateway' AND status = 'success' AND service in ['api-gateway']",
});
@@ -362,7 +362,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expect(result).toStrictEqual({
expression:
"service in ['api-gateway', 'user-service'] AND user_id exists AND has(tags, 'production') AND duration > 100 AND status NOT IN ['error', 'timeout'] AND method = 'POST'",
});
@@ -412,7 +412,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expect(result).toStrictEqual({
expression:
"count = 0 AND score > 100 AND limit >= 50 AND threshold < 1000 AND max_value <= 999 AND values in ['1', '2', '3', '4', '5']",
});
@@ -456,7 +456,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expect(result).toStrictEqual({
expression:
"is_active = true AND is_deleted = false AND email = 'user@example.com' AND description = 'Contains \"quotes\" and \\'apostrophes\\'' AND path = '/api/v1/users/123?filter=true'",
});
@@ -506,7 +506,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expect(result).toStrictEqual({
expression:
"has(tags, 'production') AND hasAny(labels, ['env:prod', 'service:api']) AND hasAll(metadata, ['version:1.0', 'team:backend']) AND services in ['api-gateway', 'user-service', 'auth-service', 'payment-service'] AND excluded_services NOT IN ['legacy-service', 'deprecated-service'] AND status_codes in ['200', '201', '400', '500']",
});
@@ -544,7 +544,7 @@ describe('convertFiltersToExpression', () => {
};
const result = convertFiltersToExpression(filters);
expect(result).toEqual({
expect(result).toStrictEqual({
expression:
"user_id NOT EXISTS AND description NOT CONTAINS 'error' AND NOT has(tags, 'production') AND NOT hasAny(labels, ['env:prod', 'service:api'])",
});
@@ -565,10 +565,9 @@ describe('convertFiltersToExpression', () => {
const result = convertFiltersToExpressionWithExistingQuery(
filters,
undefined,
);
expect(result.filters).toEqual(filters);
expect(result.filters).toStrictEqual(filters);
expect(result.filter.expression).toBe("service.name = 'test-service'");
});
@@ -580,10 +579,9 @@ describe('convertFiltersToExpression', () => {
const result = convertFiltersToExpressionWithExistingQuery(
filters,
undefined,
);
expect(result.filters).toEqual(filters);
expect(result.filters).toStrictEqual(filters);
expect(result.filter.expression).toBe('');
});
@@ -611,7 +609,7 @@ describe('convertFiltersToExpression', () => {
expect(result.filter).toBeDefined();
expect(result.filter.expression).toBe("service.name = 'updated-service'");
// Ensure parser can parse the existing query
expect(extractQueryPairs(existingQuery)).toEqual(
expect(extractQueryPairs(existingQuery)).toStrictEqual(
expect.arrayContaining([
expect.objectContaining({
key: 'service.name',
@@ -805,7 +803,7 @@ describe('convertAggregationToExpression', () => {
temporality: 'delta',
});
expect(result).toEqual([
expect(result).toStrictEqual([
{
metricName: 'test_metric',
timeAggregation: 'avg',
@@ -825,7 +823,7 @@ describe('convertAggregationToExpression', () => {
spaceAggregation: 'noop',
});
expect(result).toEqual([
expect(result).toStrictEqual([
{
metricName: 'test_metric',
timeAggregation: 'count',
@@ -841,7 +839,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.METRICS,
});
expect(result).toEqual([
expect(result).toStrictEqual([
{
metricName: '',
timeAggregation: 'sum',
@@ -858,7 +856,7 @@ describe('convertAggregationToExpression', () => {
alias: 'trace_alias',
});
expect(result).toEqual([
expect(result).toStrictEqual([
{
expression: 'count(test_metric)',
alias: 'trace_alias',
@@ -874,7 +872,7 @@ describe('convertAggregationToExpression', () => {
alias: 'log_alias',
});
expect(result).toEqual([
expect(result).toStrictEqual([
{
expression: 'avg(test_metric)',
alias: 'log_alias',
@@ -889,7 +887,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.TRACES,
});
expect(result).toEqual([
expect(result).toStrictEqual([
{
expression: 'count()',
},
@@ -903,7 +901,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.LOGS,
});
expect(result).toEqual([
expect(result).toStrictEqual([
{
expression: 'sum(test_metric)',
},
@@ -917,7 +915,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.METRICS,
});
expect(result).toEqual([
expect(result).toStrictEqual([
{
metricName: 'test_metric',
timeAggregation: 'max',
@@ -933,7 +931,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.METRICS,
});
expect(result).toEqual([
expect(result).toStrictEqual([
{
metricName: 'test_metric',
timeAggregation: 'sum',
@@ -951,7 +949,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.TRACES,
});
expect(result).toEqual([
expect(result).toStrictEqual([
{
expression: 'count()',
},
@@ -965,7 +963,7 @@ describe('convertAggregationToExpression', () => {
dataSource: DataSource.LOGS,
});
expect(result).toEqual([
expect(result).toStrictEqual([
{
expression: 'count()',
},

View File

@@ -58,7 +58,7 @@ const formatSingleValue = (v: string | number | boolean): string => {
return v;
}
// Quote and escape single quotes in strings
return `'${v.replace(/'/g, "\\'")}'`;
return `'${v.replaceAll(/'/g, "\\'")}'`;
}
// Convert numbers and booleans to strings without quotes
return String(v);

View File

@@ -471,6 +471,6 @@ describe('CheckboxFilter - User Flows', () => {
expect(filterForServiceName.key.key).toBe(SERVICE_NAME_KEY);
expect(filterForServiceName.op).toBe('in');
expect(filterForServiceName.value).toEqual(['mq-kafka', 'otel-demo']);
expect(filterForServiceName.value).toStrictEqual(['mq-kafka', 'otel-demo']);
});
});

View File

@@ -30,7 +30,7 @@ import { isKeyMatch } from './utils';
import './Checkbox.styles.scss';
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
const SELECTED_OPERATORS = new Set([OPERATORS['='], 'in']);
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in'];
const SOURCES_WITH_EMPTY_STATE_ENABLED = [QuickFiltersSource.LOGS_EXPLORER];
@@ -194,7 +194,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
);
if (filterSync) {
if (SELECTED_OPERATORS.includes(filterSync.op)) {
if (SELECTED_OPERATORS.has(filterSync.op)) {
if (isArray(filterSync.value)) {
filterSync.value.forEach((val) => {
filterState[String(val)] = true;
@@ -532,14 +532,12 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
...currentQuery.builder.queryData.map((q, idx) => {
queryData: currentQuery.builder.queryData.map((q, idx) => {
if (idx === activeQueryIndex) {
return query;
}
return q;
}),
],
},
};
@@ -553,7 +551,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
const isEmptyStateWithDocsEnabled =
SOURCES_WITH_EMPTY_STATE_ENABLED.includes(source) &&
!searchText &&
!attributeValues.length;
attributeValues.length === 0;
return (
<div className="checkbox-filter">
@@ -577,7 +575,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
<Typography.Text className="title">{filter.title}</Typography.Text>
</section>
<section className="right-action">
{isOpen && !!attributeValues.length && (
{isOpen && attributeValues.length > 0 && (
<Typography.Text
className="clear-all"
onClick={(e): void => {
@@ -593,7 +591,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
</section>
{isOpen &&
(isLoading || isLoadingKeyValueSuggestions) &&
!attributeValues.length && (
attributeValues.length === 0 && (
<section className="loading">
<Skeleton paragraph={{ rows: 4 }} />
</section>

View File

@@ -139,7 +139,7 @@ function Duration({
attribute as AllTraceFilterKeys,
)
) {
if (!values || !values.length) {
if (!values || values.length === 0) {
return [];
}
let minValue = '';

View File

@@ -92,7 +92,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
}
return currentQuery.builder.queryData.map((query, index) => ({
label: query.queryName || String.fromCharCode(65 + index),
label: query.queryName || String.fromCodePoint(65 + index),
value: index,
}));
}, [currentQuery?.builder?.queryData]);

View File

@@ -323,7 +323,7 @@ describe('Quick Filters with custom filters', () => {
const settingsButton = icon.closest('button') ?? icon;
await user.click(settingsButton);
expect(await screen.findByText('Edit quick filters')).toBeInTheDocument();
await expect(screen.findByText('Edit quick filters')).resolves.toBeInTheDocument();
const addedSection = screen.getByText(ADDED_FILTERS_LABEL).parentElement!;
expect(addedSection).toContainElement(
@@ -454,7 +454,7 @@ describe('Quick Filters with custom filters', () => {
});
const requestBody = putHandler.mock.calls[0][0];
expect(requestBody.filters).toEqual(
expect(requestBody.filters).toStrictEqual(
expect.arrayContaining([
expect.not.objectContaining({ key: FILTER_OS_DESCRIPTION }),
]),
@@ -535,12 +535,12 @@ describe('Quick Filters refetch behavior', () => {
);
const { unmount } = render(<TestQuickFilters signal={SIGNAL} />);
expect(await screen.findByText(FILTER_SERVICE_NAME)).toBeInTheDocument();
await expect(screen.findByText(FILTER_SERVICE_NAME)).resolves.toBeInTheDocument();
unmount();
render(<TestQuickFilters signal={SIGNAL} />);
expect(await screen.findByText(FILTER_SERVICE_NAME)).toBeInTheDocument();
await expect(screen.findByText(FILTER_SERVICE_NAME)).resolves.toBeInTheDocument();
expect(getCalls).toBe(2);
});
@@ -578,7 +578,7 @@ describe('Quick Filters refetch behavior', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<TestQuickFilters signal={SIGNAL} />);
expect(await screen.findByText(FILTER_SERVICE_NAME)).toBeInTheDocument();
await expect(screen.findByText(FILTER_SERVICE_NAME)).resolves.toBeInTheDocument();
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
const settingsButton = icon.closest('button') ?? icon;
@@ -628,7 +628,7 @@ describe('Quick Filters refetch behavior', () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(<TestQuickFilters signal={SIGNAL} />);
expect(await screen.findByText(FILTER_SERVICE_NAME)).toBeInTheDocument();
await expect(screen.findByText(FILTER_SERVICE_NAME)).resolves.toBeInTheDocument();
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
const settingsButton = icon.closest('button') ?? icon;
@@ -657,6 +657,6 @@ describe('Quick Filters refetch behavior', () => {
render(<TestQuickFilters signal={SIGNAL} config={[]} />);
expect(await screen.findByText('No filters found')).toBeInTheDocument();
await expect(screen.findByText('No filters found')).resolves.toBeInTheDocument();
});
});

View File

@@ -19,8 +19,8 @@ const getFilterName = (str: string): string => {
// replace . and _ with space
// capitalize the first letter of each word
return str
.replace(/\./g, ' ')
.replace(/_/g, ' ')
.replaceAll(/\./g, ' ')
.replaceAll(/_/g, ' ')
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');

View File

@@ -24,7 +24,7 @@ function RefreshPaymentStatus({
try {
await refreshPaymentStatus();
await Promise.all([activeLicenseRefetch()]);
[await activeLicenseRefetch()];
} catch (e) {
console.error(e);
}

View File

@@ -49,9 +49,9 @@ function DynamicColumnTable({
setColumnsData((prevColumns) =>
prevColumns
? [
...prevColumns.slice(0, prevColumns.length - 1),
...prevColumns.slice(0, - 1),
...visibleColumns,
prevColumns[prevColumns.length - 1],
prevColumns.at(-1),
]
: undefined,
);

View File

@@ -68,9 +68,9 @@ export const getNewColumnData: GetNewColumnDataFunction = ({
if (checked && dynamicColumns) {
return prevColumns
? [
...prevColumns.slice(0, prevColumns.length - 1),
...prevColumns.slice(0, - 1),
dynamicColumns[index],
prevColumns[prevColumns.length - 1],
prevColumns.at(-1),
]
: undefined;
}

View File

@@ -28,7 +28,7 @@ const testRoutes: RouteTabProps['routes'] = [
];
describe('RouteTab component', () => {
test('renders correctly', () => {
it('renders correctly', () => {
const history = createMemoryHistory();
render(
<Router history={history}>
@@ -39,7 +39,7 @@ describe('RouteTab component', () => {
expect(screen.getByRole('tab', { name: 'Tab2' })).toBeInTheDocument();
});
test('renders correct number of tabs', () => {
it('renders correct number of tabs', () => {
const history = createMemoryHistory();
render(
<Router history={history}>
@@ -47,10 +47,10 @@ describe('RouteTab component', () => {
</Router>,
);
const tabs = screen.getAllByRole('tab');
expect(tabs.length).toBe(testRoutes.length);
expect(tabs).toHaveLength(testRoutes.length);
});
test('sets provided activeKey as active tab', () => {
it('sets provided activeKey as active tab', () => {
const history = createMemoryHistory();
render(
<Router history={history}>
@@ -62,7 +62,7 @@ describe('RouteTab component', () => {
).toBeInTheDocument();
});
test('navigates to correct route on tab click', () => {
it('navigates to correct route on tab click', () => {
const history = createMemoryHistory();
render(
<Router history={history}>
@@ -74,7 +74,7 @@ describe('RouteTab component', () => {
expect(history.location.pathname).toBe('/tab2');
});
test('calls onChangeHandler on tab change', () => {
it('calls onChangeHandler on tab change', () => {
const onChangeHandler = jest.fn();
const history = createMemoryHistory();
render(

View File

@@ -70,9 +70,7 @@ describe('EditKeyModal (URL-controlled)', () => {
it('renders key data from prop when edit-key param is set', async () => {
renderModal();
expect(
await screen.findByDisplayValue('Original Key Name'),
).toBeInTheDocument();
await expect(screen.findByDisplayValue('Original Key Name')).resolves.toBeInTheDocument();
expect(screen.getByRole('button', { name: /Save Changes/i })).toBeDisabled();
});
@@ -110,8 +108,8 @@ describe('EditKeyModal (URL-controlled)', () => {
});
const latestUrlUpdate =
onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]?.[0];
expect(latestUrlUpdate).toEqual(
onUrlUpdate.mock.calls.at(-1)?.[0];
expect(latestUrlUpdate).toStrictEqual(
expect.objectContaining({
queryString: expect.any(String),
}),
@@ -134,9 +132,7 @@ describe('EditKeyModal (URL-controlled)', () => {
await user.click(screen.getByRole('button', { name: /Revoke Key/i }));
// Same dialog, now showing revoke confirmation
expect(
await screen.findByRole('dialog', { name: /Revoke Original Key Name/i }),
).toBeInTheDocument();
await expect(screen.findByRole('dialog', { name: /Revoke Original Key Name/i })).resolves.toBeInTheDocument();
expect(
screen.getByText(/Revoking this key will permanently invalidate it/i),
).toBeInTheDocument();

View File

@@ -104,7 +104,7 @@ describe('ServiceAccountDrawer', () => {
it('renders Overview tab by default: editable name input, locked email, Save disabled when not dirty', async () => {
renderDrawer();
expect(await screen.findByDisplayValue('CI Bot')).toBeInTheDocument();
await expect(screen.findByDisplayValue('CI Bot')).resolves.toBeInTheDocument();
expect(screen.getByText('ci-bot@signoz.io')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Save Changes/i })).toBeDisabled();
});
@@ -207,7 +207,7 @@ describe('ServiceAccountDrawer', () => {
expect(dialog).toBeInTheDocument();
const confirmBtns = screen.getAllByRole('button', { name: /^Delete$/i });
await user.click(confirmBtns[confirmBtns.length - 1]);
await user.click(confirmBtns.at(-1));
await waitFor(() => {
expect(deleteSpy).toHaveBeenCalled();
@@ -272,11 +272,9 @@ describe('ServiceAccountDrawer', () => {
renderDrawer();
expect(
await screen.findByText(
await expect(screen.findByText(
/An unexpected error occurred while fetching service account details/i,
),
).toBeInTheDocument();
)).resolves.toBeInTheDocument();
});
});
@@ -349,11 +347,9 @@ describe('ServiceAccountDrawer save-error UX', () => {
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await user.click(saveBtn);
expect(
await screen.findByText(/Name update.*name update failed/i, undefined, {
await expect(screen.findByText(/Name update.*name update failed/i, undefined, {
timeout: 5000,
}),
).toBeInTheDocument();
})).resolves.toBeInTheDocument();
});
it('role add failure shows SaveErrorItem with the role name context', async () => {
@@ -385,15 +381,13 @@ describe('ServiceAccountDrawer save-error UX', () => {
await waitFor(() => expect(saveBtn).not.toBeDisabled());
await user.click(saveBtn);
expect(
await screen.findByText(
await expect(screen.findByText(
/Role 'signoz-viewer'.*role assign failed/i,
undefined,
{
timeout: 5000,
},
),
).toBeInTheDocument();
)).resolves.toBeInTheDocument();
});
it('role add retries on 429 then succeeds without showing an error', async () => {

View File

@@ -83,7 +83,7 @@ describe('useShiftHoldOverlay', () => {
it('does not activate in typing context (input)', () => {
const input = document.createElement('input');
document.body.appendChild(input);
document.body.append(input);
const { result } = renderHook(() => useShiftHoldOverlay({}));

View File

@@ -1,135 +0,0 @@
import { ComponentProps, memo } from 'react';
import { TableComponents } from 'react-virtuoso';
import cx from 'classnames';
import TanStackRowCells from './TanStackRow';
import {
useClearRowHovered,
useSetRowHovered,
} from './TanStackTableStateContext';
import { FlatItem, TableRowContext } from './types';
import tableStyles from './TanStackTable.module.scss';
type VirtuosoTableRowProps<TData> = ComponentProps<
NonNullable<
TableComponents<FlatItem<TData>, TableRowContext<TData>>['TableRow']
>
>;
function TanStackCustomTableRow<TData>({
item,
context,
...props
}: VirtuosoTableRowProps<TData>): JSX.Element {
const rowId = item.row.id;
const rowData = item.row.original;
// Stable callbacks for hover state management
const setHovered = useSetRowHovered(rowId);
const clearHovered = useClearRowHovered(rowId);
if (item.kind === 'expansion') {
return (
<tr {...props} className={tableStyles.tableRowExpansion}>
<TanStackRowCells
row={item.row}
itemKind={item.kind}
context={context}
hasSingleColumn={context?.hasSingleColumn ?? false}
columnOrderKey={context?.columnOrderKey ?? ''}
columnVisibilityKey={context?.columnVisibilityKey ?? ''}
/>
</tr>
);
}
const isActive = context?.isRowActive?.(rowData) ?? false;
const extraClass = context?.getRowClassName?.(rowData) ?? '';
const rowStyle = context?.getRowStyle?.(rowData);
const rowClassName = cx(
tableStyles.tableRow,
isActive && tableStyles.tableRowActive,
extraClass,
);
return (
<tr
{...props}
className={rowClassName}
style={rowStyle}
onMouseEnter={setHovered}
onMouseLeave={clearHovered}
>
<TanStackRowCells
row={item.row}
itemKind={item.kind}
context={context}
hasSingleColumn={context?.hasSingleColumn ?? false}
columnOrderKey={context?.columnOrderKey ?? ''}
columnVisibilityKey={context?.columnVisibilityKey ?? ''}
/>
</tr>
);
}
// Custom comparison - only re-render when row identity or computed values change
// This looks overkill but ensures the table is stable and doesn't re-render on every change
// If you add any new prop to context, remember to update this function
// eslint-disable-next-line sonarjs/cognitive-complexity
function areTableRowPropsEqual<TData>(
prev: Readonly<VirtuosoTableRowProps<TData>>,
next: Readonly<VirtuosoTableRowProps<TData>>,
): boolean {
if (prev.item.row.id !== next.item.row.id) {
return false;
}
if (prev.item.kind !== next.item.kind) {
return false;
}
const prevData = prev.item.row.original;
const nextData = next.item.row.original;
if (prevData !== nextData) {
return false;
}
if (prev.context?.hasSingleColumn !== next.context?.hasSingleColumn) {
return false;
}
if (prev.context?.columnOrderKey !== next.context?.columnOrderKey) {
return false;
}
if (prev.context?.columnVisibilityKey !== next.context?.columnVisibilityKey) {
return false;
}
if (prev.context !== next.context) {
const prevActive = prev.context?.isRowActive?.(prevData) ?? false;
const nextActive = next.context?.isRowActive?.(nextData) ?? false;
if (prevActive !== nextActive) {
return false;
}
const prevClass = prev.context?.getRowClassName?.(prevData) ?? '';
const nextClass = next.context?.getRowClassName?.(nextData) ?? '';
if (prevClass !== nextClass) {
return false;
}
const prevStyle = prev.context?.getRowStyle?.(prevData);
const nextStyle = next.context?.getRowStyle?.(nextData);
if (prevStyle !== nextStyle) {
return false;
}
}
return true;
}
export default memo(
TanStackCustomTableRow,
areTableRowPropsEqual,
) as typeof TanStackCustomTableRow;

View File

@@ -1,260 +0,0 @@
import type {
CSSProperties,
MouseEvent as ReactMouseEvent,
TouchEvent as ReactTouchEvent,
} from 'react';
import { useCallback, useMemo } from 'react';
import { CloseOutlined, MoreOutlined } from '@ant-design/icons';
import { useSortable } from '@dnd-kit/sortable';
import { Popover, PopoverContent, PopoverTrigger } from '@signozhq/ui';
import { flexRender, Header as TanStackHeader } from '@tanstack/react-table';
import cx from 'classnames';
import { ChevronDown, ChevronUp, GripVertical } from 'lucide-react';
import { SortState, TableColumnDef } from './types';
import headerStyles from './TanStackHeaderRow.module.scss';
import tableStyles from './TanStackTable.module.scss';
type TanStackHeaderRowProps<TData = unknown> = {
column: TableColumnDef<TData>;
header?: TanStackHeader<TData, unknown>;
isDarkMode: boolean;
hasSingleColumn: boolean;
canRemoveColumn?: boolean;
onRemoveColumn?: (columnId: string) => void;
orderBy?: SortState | null;
onSort?: (sort: SortState | null) => void;
/** Last column cannot be resized */
isLastColumn?: boolean;
};
const GRIP_ICON_SIZE = 12;
const SORT_ICON_SIZE = 14;
// eslint-disable-next-line sonarjs/cognitive-complexity
function TanStackHeaderRow<TData>({
column,
header,
isDarkMode,
hasSingleColumn,
canRemoveColumn = false,
onRemoveColumn,
orderBy,
onSort,
isLastColumn = false,
}: TanStackHeaderRowProps<TData>): JSX.Element {
const columnId = column.id;
const isDragColumn = column.enableMove !== false && column.pin == null;
const isResizableColumn =
!isLastColumn &&
column.enableResize !== false &&
Boolean(header?.column.getCanResize());
const isColumnRemovable = Boolean(
canRemoveColumn && onRemoveColumn && column.enableRemove,
);
const isSortable = column.enableSort === true && Boolean(onSort);
const currentSortDirection =
orderBy?.columnName === columnId ? orderBy.order : null;
const isResizing = Boolean(header?.column.getIsResizing());
const resizeHandler = header?.getResizeHandler();
const headerText =
typeof column.header === 'string' && column.header
? column.header
: String(header?.id ?? columnId);
const headerTitleAttr = headerText.replace(/^\w/, (c) => c.toUpperCase());
const handleSortClick = useCallback((): void => {
if (!isSortable || !onSort) {
return;
}
if (currentSortDirection === null) {
onSort({ columnName: columnId, order: 'asc' });
} else if (currentSortDirection === 'asc') {
onSort({ columnName: columnId, order: 'desc' });
} else {
onSort(null);
}
}, [isSortable, onSort, currentSortDirection, columnId]);
const handleResizeStart = (
event: ReactMouseEvent<HTMLElement> | ReactTouchEvent<HTMLElement>,
): void => {
event.preventDefault();
event.stopPropagation();
resizeHandler?.(event);
};
const {
attributes,
listeners,
setNodeRef,
setActivatorNodeRef,
transform,
transition,
isDragging,
} = useSortable({
id: columnId,
disabled: !isDragColumn,
});
const headerCellStyle = useMemo(
() =>
({
'--tanstack-header-translate-x': `${Math.round(transform?.x ?? 0)}px`,
'--tanstack-header-translate-y': `${Math.round(transform?.y ?? 0)}px`,
'--tanstack-header-transition': isResizing ? 'none' : transition || 'none',
}) as CSSProperties,
[isResizing, transform?.x, transform?.y, transition],
);
const headerCellClassName = cx(
headerStyles.tanstackHeaderCell,
isDragging && headerStyles.isDragging,
isResizing && headerStyles.isResizing,
);
const headerContentClassName = cx(
headerStyles.tanstackHeaderContent,
isResizableColumn && headerStyles.hasResizeControl,
isColumnRemovable && headerStyles.hasActionControl,
isSortable && headerStyles.isSortable,
);
const thClassName = cx(
tableStyles.tableHeaderCell,
headerCellClassName,
column.id,
);
return (
<th
ref={setNodeRef}
className={thClassName}
key={columnId}
style={headerCellStyle}
data-dark-mode={isDarkMode}
data-single-column={hasSingleColumn || undefined}
>
<span className={headerContentClassName}>
{isDragColumn ? (
<span className={headerStyles.tanstackGripSlot}>
<span
ref={setActivatorNodeRef}
{...attributes}
{...listeners}
role="button"
aria-label={`Drag ${String(
(typeof column.header === 'string' && column.header) ||
header?.id ||
columnId,
)} column`}
className={headerStyles.tanstackGripActivator}
>
<GripVertical size={GRIP_ICON_SIZE} />
</span>
</span>
) : null}
{isSortable ? (
<button
type="button"
className={cx(
'tanstack-header-title',
headerStyles.tanstackSortButton,
currentSortDirection && headerStyles.isSorted,
)}
title={headerTitleAttr}
onClick={handleSortClick}
data-sort={
currentSortDirection === 'asc'
? 'ascending'
: currentSortDirection === 'desc'
? 'descending'
: 'none'
}
>
<span className={headerStyles.tanstackSortLabel}>
{header?.column?.columnDef
? flexRender(header.column.columnDef.header, header.getContext())
: typeof column.header === 'function'
? column.header()
: String(column.header || '').replace(/^\w/, (c) => c.toUpperCase())}
</span>
<span className={headerStyles.tanstackSortIndicator}>
{currentSortDirection === 'asc' ? (
<ChevronUp size={SORT_ICON_SIZE} />
) : currentSortDirection === 'desc' ? (
<ChevronDown size={SORT_ICON_SIZE} />
) : null}
</span>
</button>
) : (
<span
className={cx('tanstack-header-title', headerStyles.tanstackHeaderTitle)}
title={headerTitleAttr}
>
{header?.column?.columnDef
? flexRender(header.column.columnDef.header, header.getContext())
: typeof column.header === 'function'
? column.header()
: String(column.header || '').replace(/^\w/, (c) => c.toUpperCase())}
</span>
)}
{isColumnRemovable && (
<Popover>
<PopoverTrigger asChild>
<span
role="button"
aria-label={`Column actions for ${headerTitleAttr}`}
className={headerStyles.tanstackHeaderActionTrigger}
onMouseDown={(event): void => {
event.stopPropagation();
}}
>
<MoreOutlined />
</span>
</PopoverTrigger>
<PopoverContent
align="end"
sideOffset={6}
className={headerStyles.tanstackColumnActionsContent}
>
<button
type="button"
className={headerStyles.tanstackRemoveColumnAction}
onClick={(event): void => {
event.preventDefault();
event.stopPropagation();
onRemoveColumn?.(column.id);
}}
>
<CloseOutlined
className={headerStyles.tanstackRemoveColumnActionIcon}
/>
Remove column
</button>
</PopoverContent>
</Popover>
)}
</span>
{isResizableColumn && (
<span
role="presentation"
className={headerStyles.cursorColResize}
title="Drag to resize column"
onClick={(event): void => {
event.preventDefault();
event.stopPropagation();
}}
onMouseDown={(event): void => {
handleResizeStart(event);
}}
onTouchStart={(event): void => {
handleResizeStart(event);
}}
>
<span className={headerStyles.tanstackResizeHandleLine} />
</span>
)}
</th>
);
}
export default TanStackHeaderRow;

View File

@@ -1,136 +0,0 @@
import type { MouseEvent } from 'react';
import { memo, useCallback } from 'react';
import { Row as TanStackRowModel } from '@tanstack/react-table';
import { TanStackRowCell } from './TanStackRowCell';
import { useIsRowHovered } from './TanStackTableStateContext';
import { TableRowContext } from './types';
import tableStyles from './TanStackTable.module.scss';
type TanStackRowCellsProps<TData> = {
row: TanStackRowModel<TData>;
context: TableRowContext<TData> | undefined;
itemKind: 'row' | 'expansion';
hasSingleColumn: boolean;
columnOrderKey: string;
columnVisibilityKey: string;
};
function TanStackRowCellsInner<TData>({
row,
context,
itemKind,
hasSingleColumn,
columnOrderKey: _columnOrderKey,
columnVisibilityKey: _columnVisibilityKey,
}: TanStackRowCellsProps<TData>): JSX.Element {
const hasHovered = useIsRowHovered(row.id);
const rowData = row.original;
const visibleCells = row.getVisibleCells();
const lastCellIndex = visibleCells.length - 1;
// Stable references via destructuring, keep them as is
const onRowClick = context?.onRowClick;
const onRowClickNewTab = context?.onRowClickNewTab;
const onRowDeactivate = context?.onRowDeactivate;
const isRowActive = context?.isRowActive;
const getRowKeyData = context?.getRowKeyData;
const rowIndex = row.index;
const handleClick = useCallback(
(event: MouseEvent<HTMLTableCellElement>) => {
const keyData = getRowKeyData?.(rowIndex);
const itemKey = keyData?.itemKey ?? '';
// Handle ctrl+click or cmd+click (open in new tab)
if ((event.ctrlKey || event.metaKey) && onRowClickNewTab) {
onRowClickNewTab(rowData, itemKey);
return;
}
const isActive = isRowActive?.(rowData) ?? false;
if (isActive && onRowDeactivate) {
onRowDeactivate();
} else {
onRowClick?.(rowData, itemKey);
}
},
[
isRowActive,
onRowDeactivate,
onRowClick,
onRowClickNewTab,
rowData,
getRowKeyData,
rowIndex,
],
);
if (itemKind === 'expansion') {
const keyData = getRowKeyData?.(rowIndex);
return (
<td
colSpan={context?.colCount ?? 1}
className={tableStyles.tableCellExpansion}
>
{context?.renderExpandedRow?.(
rowData,
keyData?.finalKey ?? '',
keyData?.groupMeta,
)}
</td>
);
}
return (
<>
{visibleCells.map((cell, index) => {
const isLastCell = index === lastCellIndex;
return (
<TanStackRowCell
key={cell.id}
cell={cell}
hasSingleColumn={hasSingleColumn}
isLastCell={isLastCell}
hasHovered={hasHovered}
rowData={rowData}
onClick={handleClick}
renderRowActions={context?.renderRowActions}
/>
);
})}
</>
);
}
// Custom comparison - only re-render when row data changes
// If you add any new prop to context, remember to update this function
function areRowCellsPropsEqual<TData>(
prev: Readonly<TanStackRowCellsProps<TData>>,
next: Readonly<TanStackRowCellsProps<TData>>,
): boolean {
return (
prev.row.id === next.row.id &&
prev.itemKind === next.itemKind &&
prev.hasSingleColumn === next.hasSingleColumn &&
prev.columnOrderKey === next.columnOrderKey &&
prev.columnVisibilityKey === next.columnVisibilityKey &&
prev.context?.onRowClick === next.context?.onRowClick &&
prev.context?.onRowClickNewTab === next.context?.onRowClickNewTab &&
prev.context?.onRowDeactivate === next.context?.onRowDeactivate &&
prev.context?.isRowActive === next.context?.isRowActive &&
prev.context?.getRowKeyData === next.context?.getRowKeyData &&
prev.context?.renderRowActions === next.context?.renderRowActions &&
prev.context?.renderExpandedRow === next.context?.renderExpandedRow &&
prev.context?.colCount === next.context?.colCount
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const TanStackRowCells = memo(
TanStackRowCellsInner,
areRowCellsPropsEqual as any,
) as <T>(props: TanStackRowCellsProps<T>) => JSX.Element;
export default TanStackRowCells;

View File

@@ -1,88 +0,0 @@
import type { MouseEvent, ReactNode } from 'react';
import { memo } from 'react';
import type { Cell } from '@tanstack/react-table';
import { flexRender } from '@tanstack/react-table';
import { Skeleton } from 'antd';
import cx from 'classnames';
import { useShouldShowCellSkeleton } from './TanStackTableStateContext';
import tableStyles from './TanStackTable.module.scss';
import skeletonStyles from './TanStackTableSkeleton.module.scss';
export type TanStackRowCellProps<TData> = {
cell: Cell<TData, unknown>;
hasSingleColumn: boolean;
isLastCell: boolean;
hasHovered: boolean;
rowData: TData;
onClick: (event: MouseEvent<HTMLTableCellElement>) => void;
renderRowActions?: (row: TData) => ReactNode;
};
function TanStackRowCellInner<TData>({
cell,
hasSingleColumn,
isLastCell,
hasHovered,
rowData,
onClick,
renderRowActions,
}: TanStackRowCellProps<TData>): JSX.Element {
const showSkeleton = useShouldShowCellSkeleton();
return (
<td
className={cx(tableStyles.tableCell, 'tanstack-cell-' + cell.column.id)}
data-single-column={hasSingleColumn || undefined}
onClick={onClick}
>
{showSkeleton ? (
<Skeleton.Input
active
size="small"
className={skeletonStyles.cellSkeleton}
/>
) : (
flexRender(cell.column.columnDef.cell, cell.getContext())
)}
{isLastCell && hasHovered && renderRowActions && !showSkeleton && (
<span className={tableStyles.tableViewRowActions}>
{renderRowActions(rowData)}
</span>
)}
</td>
);
}
// Custom comparison - only re-render when row data changes
// If you add any new prop to context, remember to update this function
function areTanStackRowCellPropsEqual<TData>(
prev: Readonly<TanStackRowCellProps<TData>>,
next: Readonly<TanStackRowCellProps<TData>>,
): boolean {
if (next.cell.id.startsWith('skeleton-')) {
return false;
}
return (
prev.cell.id === next.cell.id &&
prev.cell.column.id === next.cell.column.id &&
Object.is(prev.cell.getValue(), next.cell.getValue()) &&
prev.hasSingleColumn === next.hasSingleColumn &&
prev.isLastCell === next.isLastCell &&
prev.hasHovered === next.hasHovered &&
prev.onClick === next.onClick &&
prev.renderRowActions === next.renderRowActions &&
prev.rowData === next.rowData
);
}
const TanStackRowCellMemo = memo(
TanStackRowCellInner,
areTanStackRowCellPropsEqual,
);
TanStackRowCellMemo.displayName = 'TanStackRowCell';
export const TanStackRowCell = TanStackRowCellMemo as typeof TanStackRowCellInner;

View File

@@ -1,100 +0,0 @@
.tanStackTable {
width: 100%;
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
& td,
& th {
overflow: hidden;
min-width: 0;
box-sizing: border-box;
vertical-align: middle;
}
}
.tableCellText {
font-style: normal;
font-weight: 400;
letter-spacing: -0.07px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
width: auto;
-webkit-box-orient: vertical;
-webkit-line-clamp: var(--tanstack-plain-body-line-clamp, 1);
line-clamp: var(--tanstack-plain-body-line-clamp, 1);
font-size: var(--tanstack-plain-cell-font-size, 14px);
line-height: var(--tanstack-plain-cell-line-height, 18px);
color: var(--l2-foreground);
max-width: 100%;
word-break: break-all;
}
.tableViewRowActions {
position: absolute;
top: 50%;
right: 8px;
left: auto;
transform: translateY(-50%);
margin: 0;
z-index: 5;
}
.tableCell {
padding: 0.3rem;
font-style: normal;
font-weight: 400;
letter-spacing: -0.07px;
font-size: var(--tanstack-plain-cell-font-size, 14px);
line-height: var(--tanstack-plain-cell-line-height, 18px);
color: var(--l2-foreground);
}
.tableRow {
cursor: pointer;
position: relative;
overflow-anchor: none;
&:hover {
.tableCell {
background-color: var(--row-hover-bg) !important;
}
}
&.tableRowActive {
.tableCell {
background-color: var(--row-active-bg) !important;
}
}
}
.tableHeaderCell {
padding: 0.3rem;
height: 36px;
text-align: left;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 18px;
letter-spacing: -0.07px;
color: var(--l1-foreground);
// TODO: Remove this once background color (l1) is matching the actual background color of the page
&[data-dark-mode='true'] {
background: #0b0c0d;
}
&[data-dark-mode='false'] {
background: #fdfdfd;
}
}
.tableRowExpansion {
display: table-row;
}
.tableCellExpansion {
padding: 0.5rem;
vertical-align: top;
}

View File

@@ -1,572 +0,0 @@
import type { ComponentProps, CSSProperties } from 'react';
import {
forwardRef,
memo,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
} from 'react';
import type { TableComponents } from 'react-virtuoso';
import { TableVirtuoso, TableVirtuosoHandle } from 'react-virtuoso';
import { LoadingOutlined } from '@ant-design/icons';
import { DndContext, pointerWithin } from '@dnd-kit/core';
import {
horizontalListSortingStrategy,
SortableContext,
} from '@dnd-kit/sortable';
import {
ComboboxSimple,
ComboboxSimpleItem,
TooltipProvider,
} from '@signozhq/ui';
import { Pagination } from '@signozhq/ui';
import type { Row } from '@tanstack/react-table';
import {
ColumnDef,
ColumnPinningState,
getCoreRowModel,
useReactTable,
} from '@tanstack/react-table';
import { Spin } from 'antd';
import cx from 'classnames';
import { useIsDarkMode } from 'hooks/useDarkMode';
import TanStackCustomTableRow from './TanStackCustomTableRow';
import TanStackHeaderRow from './TanStackHeaderRow';
import {
ColumnVisibilitySync,
TableLoadingSync,
TanStackTableStateProvider,
} from './TanStackTableStateContext';
import {
FlatItem,
TableRowContext,
TanStackTableHandle,
TanStackTableProps,
} from './types';
import { useColumnDnd } from './useColumnDnd';
import { useColumnHandlers } from './useColumnHandlers';
import { useColumnState } from './useColumnState';
import { useEffectiveData } from './useEffectiveData';
import { useFlatItems } from './useFlatItems';
import { useRowKeyData } from './useRowKeyData';
import { useTableParams } from './useTableParams';
import { buildTanstackColumnDef } from './utils';
import { VirtuosoTableColGroup } from './VirtuosoTableColGroup';
import tableStyles from './TanStackTable.module.scss';
import viewStyles from './TanStackTableView.module.scss';
const COLUMN_DND_AUTO_SCROLL = {
layoutShiftCompensation: false as const,
threshold: { x: 0.2, y: 0 },
};
const INCREASE_VIEWPORT_BY = { top: 500, bottom: 500 };
const noopColumnVisibility = (): void => {};
const paginationPageSizeItems: ComboboxSimpleItem[] = [10, 20, 30, 50, 100].map(
(value) => ({
value: value.toString(),
label: value.toString(),
displayValue: value.toString(),
}),
);
// eslint-disable-next-line sonarjs/cognitive-complexity
function TanStackTableInner<TData>(
{
data,
columns,
columnStorageKey,
columnSizing: columnSizingProp,
onColumnSizingChange,
onColumnOrderChange,
onColumnRemove,
isLoading = false,
skeletonRowCount = 10,
enableQueryParams,
pagination,
onEndReached,
getRowKey,
getItemKey,
groupBy,
getGroupKey,
getRowStyle,
getRowClassName,
isRowActive,
renderRowActions,
onRowClick,
onRowClickNewTab,
onRowDeactivate,
activeRowIndex,
renderExpandedRow,
getRowCanExpand,
tableScrollerProps,
plainTextCellLineClamp,
cellTypographySize,
className,
testId,
prefixPaginationContent,
suffixPaginationContent,
}: TanStackTableProps<TData>,
forwardedRef: React.ForwardedRef<TanStackTableHandle>,
): JSX.Element {
const virtuosoRef = useRef<TableVirtuosoHandle | null>(null);
const isDarkMode = useIsDarkMode();
const {
page,
limit,
setPage,
setLimit,
orderBy,
setOrderBy,
expanded,
setExpanded,
} = useTableParams(enableQueryParams, {
page: pagination?.defaultPage,
limit: pagination?.defaultLimit,
});
const isGrouped = (groupBy?.length ?? 0) > 0;
const {
columnVisibility: storeVisibility,
columnSizing: storeSizing,
sortedColumns,
hideColumn,
setColumnSizing: storeSetSizing,
setColumnOrder: storeSetOrder,
} = useColumnState({
storageKey: columnStorageKey,
columns,
isGrouped,
});
// Use store values when columnStorageKey is provided, otherwise fall back to props/defaults
const effectiveColumns = columnStorageKey ? sortedColumns : columns;
const effectiveVisibility = columnStorageKey ? storeVisibility : {};
const effectiveSizing = columnStorageKey
? storeSizing
: columnSizingProp ?? {};
const effectiveData = useEffectiveData<TData>({
data,
isLoading,
limit,
skeletonRowCount,
});
const { rowKeyData, getRowKeyData } = useRowKeyData({
data: effectiveData,
isLoading,
getRowKey,
getItemKey,
groupBy,
getGroupKey,
});
const {
handleColumnSizingChange,
handleColumnOrderChange,
handleRemoveColumn,
} = useColumnHandlers({
columnStorageKey,
effectiveSizing,
storeSetSizing,
storeSetOrder,
hideColumn,
onColumnSizingChange,
onColumnOrderChange,
onColumnRemove,
});
const columnPinning = useMemo<ColumnPinningState>(
() => ({
left: effectiveColumns.filter((c) => c.pin === 'left').map((c) => c.id),
right: effectiveColumns.filter((c) => c.pin === 'right').map((c) => c.id),
}),
[effectiveColumns],
);
const tanstackColumns = useMemo<ColumnDef<TData>[]>(
() =>
effectiveColumns.map((colDef) =>
buildTanstackColumnDef(colDef, isRowActive, getRowKeyData),
),
[effectiveColumns, isRowActive, getRowKeyData],
);
const getRowId = useCallback(
(row: TData, index: number): string => {
if (rowKeyData) {
return rowKeyData[index]?.finalKey ?? String(index);
}
const r = row as Record<string, unknown>;
if (r != null && typeof r.id !== 'undefined') {
return String(r.id);
}
return String(index);
},
[rowKeyData],
);
const tableGetRowCanExpand = useCallback(
(row: Row<TData>): boolean =>
getRowCanExpand ? getRowCanExpand(row.original) : true,
[getRowCanExpand],
);
const table = useReactTable({
data: effectiveData,
columns: tanstackColumns,
enableColumnResizing: true,
enableColumnPinning: true,
columnResizeMode: 'onChange',
getCoreRowModel: getCoreRowModel(),
getRowId,
enableExpanding: Boolean(renderExpandedRow),
getRowCanExpand: renderExpandedRow ? tableGetRowCanExpand : undefined,
onColumnSizingChange: handleColumnSizingChange,
onColumnVisibilityChange: noopColumnVisibility,
onExpandedChange: setExpanded,
state: {
columnSizing: effectiveSizing,
columnVisibility: effectiveVisibility,
columnPinning,
expanded,
},
});
// Keep refs to avoid recreating virtuosoComponents on every resize/render
const tableRef = useRef(table);
tableRef.current = table;
const columnsRef = useRef(effectiveColumns);
columnsRef.current = effectiveColumns;
const tableRows = table.getRowModel().rows;
const { flatItems, flatIndexForActiveRow } = useFlatItems({
tableRows,
renderExpandedRow,
expanded,
activeRowIndex,
});
// keep previous count just to avoid flashing the pagination component
const prevTotalCountRef = useRef(pagination?.total || 0);
if (pagination?.total && pagination?.total > 0) {
prevTotalCountRef.current = pagination?.total;
}
const effectiveTotalCount = !isLoading
? pagination?.total || 0
: prevTotalCountRef.current;
useEffect(() => {
if (flatIndexForActiveRow < 0) {
return;
}
virtuosoRef.current?.scrollToIndex({
index: flatIndexForActiveRow,
align: 'center',
behavior: 'auto',
});
}, [flatIndexForActiveRow]);
const { sensors, columnIds, handleDragEnd } = useColumnDnd({
columns: effectiveColumns,
onColumnOrderChange: handleColumnOrderChange,
});
const hasSingleColumn = useMemo(
() =>
effectiveColumns.filter((c) => !c.pin && c.enableRemove !== false).length <=
1,
[effectiveColumns],
);
const canRemoveColumn = !hasSingleColumn;
const flatHeaders = useMemo(
() => table.getFlatHeaders().filter((header) => !header.isPlaceholder),
// eslint-disable-next-line react-hooks/exhaustive-deps
[tanstackColumns, columnPinning, effectiveVisibility],
);
const columnsById = useMemo(
() => new Map(effectiveColumns.map((c) => [c.id, c] as const)),
[effectiveColumns],
);
const visibleColumnsCount = table.getVisibleFlatColumns().length;
const columnOrderKey = useMemo(() => columnIds.join(','), [columnIds]);
const columnVisibilityKey = useMemo(
() =>
table
.getVisibleFlatColumns()
.map((c) => c.id)
.join(','),
// we want to explicitly have table out of this deps
// eslint-disable-next-line react-hooks/exhaustive-deps
[effectiveVisibility, columnIds],
);
const virtuosoContext = useMemo<TableRowContext<TData>>(
() => ({
getRowStyle,
getRowClassName,
isRowActive,
renderRowActions,
onRowClick,
onRowClickNewTab,
onRowDeactivate,
renderExpandedRow,
getRowKeyData,
colCount: visibleColumnsCount,
isDarkMode,
plainTextCellLineClamp,
hasSingleColumn,
columnOrderKey,
columnVisibilityKey,
}),
[
getRowStyle,
getRowClassName,
isRowActive,
renderRowActions,
onRowClick,
onRowClickNewTab,
onRowDeactivate,
renderExpandedRow,
getRowKeyData,
visibleColumnsCount,
isDarkMode,
plainTextCellLineClamp,
hasSingleColumn,
columnOrderKey,
columnVisibilityKey,
],
);
const tableHeader = useCallback(() => {
return (
<DndContext
sensors={sensors}
collisionDetection={pointerWithin}
onDragEnd={handleDragEnd}
autoScroll={COLUMN_DND_AUTO_SCROLL}
>
<SortableContext items={columnIds} strategy={horizontalListSortingStrategy}>
<tr>
{flatHeaders.map((header, index) => {
const column = columnsById.get(header.id);
if (!column) {
return null;
}
return (
<TanStackHeaderRow
key={header.id}
column={column}
header={header}
isDarkMode={isDarkMode}
hasSingleColumn={hasSingleColumn}
onRemoveColumn={handleRemoveColumn}
canRemoveColumn={canRemoveColumn}
orderBy={orderBy}
onSort={setOrderBy}
isLastColumn={index === flatHeaders.length - 1}
/>
);
})}
</tr>
</SortableContext>
</DndContext>
);
}, [
sensors,
handleDragEnd,
columnIds,
flatHeaders,
columnsById,
isDarkMode,
hasSingleColumn,
handleRemoveColumn,
canRemoveColumn,
orderBy,
setOrderBy,
]);
const handleEndReached = useCallback(
(index: number): void => {
onEndReached?.(index);
},
[onEndReached],
);
const isInfiniteScrollMode = Boolean(onEndReached);
const showInfiniteScrollLoader = isInfiniteScrollMode && isLoading;
useImperativeHandle(
forwardedRef,
(): TanStackTableHandle =>
new Proxy(
{
goToPage: (p: number): void => {
setPage(p);
virtuosoRef.current?.scrollToIndex({
index: 0,
align: 'start',
});
},
} as TanStackTableHandle,
{
get(target, prop): unknown {
if (prop in target) {
return Reflect.get(target, prop);
}
const v = (virtuosoRef.current as unknown) as Record<string, unknown>;
const value = v?.[prop as string];
if (typeof value === 'function') {
return (value as (...a: unknown[]) => unknown).bind(virtuosoRef.current);
}
return value;
},
},
),
[setPage],
);
const showPagination = Boolean(pagination && !onEndReached);
const { className: tableScrollerClassName, ...restTableScrollerProps } =
tableScrollerProps ?? {};
const cellTypographyClass = useMemo((): string | undefined => {
if (cellTypographySize === 'small') {
return viewStyles.cellTypographySmall;
}
if (cellTypographySize === 'medium') {
return viewStyles.cellTypographyMedium;
}
if (cellTypographySize === 'large') {
return viewStyles.cellTypographyLarge;
}
return undefined;
}, [cellTypographySize]);
const virtuosoClassName = useMemo(
() =>
cx(
viewStyles.tanstackTableVirtuosoScroll,
cellTypographyClass,
tableScrollerClassName,
),
[cellTypographyClass, tableScrollerClassName],
);
const virtuosoTableStyle = useMemo(
() =>
({
'--tanstack-plain-body-line-clamp': plainTextCellLineClamp,
} as CSSProperties),
[plainTextCellLineClamp],
);
type VirtuosoTableComponentProps = ComponentProps<
NonNullable<TableComponents<FlatItem<TData>, TableRowContext<TData>>['Table']>
>;
// Use refs in virtuosoComponents to keep the component reference stable during resize
// This prevents Virtuoso from re-rendering all rows when columns are resized
const virtuosoComponents = useMemo(
() => ({
Table: ({ style, children }: VirtuosoTableComponentProps): JSX.Element => (
<table className={tableStyles.tanStackTable} style={style}>
<VirtuosoTableColGroup
columns={columnsRef.current}
table={tableRef.current}
/>
{children}
</table>
),
TableRow: TanStackCustomTableRow,
}),
[],
);
return (
<div className={cx(viewStyles.tanstackTableViewWrapper, className)}>
<TanStackTableStateProvider>
<TableLoadingSync
isLoading={isLoading}
isInfiniteScrollMode={isInfiniteScrollMode}
/>
<ColumnVisibilitySync visibility={effectiveVisibility} />
<TooltipProvider>
<TableVirtuoso<FlatItem<TData>, TableRowContext<TData>>
className={virtuosoClassName}
ref={virtuosoRef}
{...restTableScrollerProps}
data={flatItems}
totalCount={flatItems.length}
context={virtuosoContext}
increaseViewportBy={INCREASE_VIEWPORT_BY}
initialTopMostItemIndex={
flatIndexForActiveRow >= 0 ? flatIndexForActiveRow : 0
}
fixedHeaderContent={tableHeader}
style={virtuosoTableStyle}
components={virtuosoComponents}
endReached={onEndReached ? handleEndReached : undefined}
data-testid={testId}
/>
{showInfiniteScrollLoader && (
<div
className={viewStyles.tanstackLoadingOverlay}
data-testid="tanstack-infinite-loader"
>
<Spin indicator={<LoadingOutlined spin />} tip="Loading more..." />
</div>
)}
{showPagination && pagination && (
<div className={viewStyles.paginationContainer}>
{prefixPaginationContent}
<Pagination
current={page}
pageSize={limit}
total={effectiveTotalCount}
onPageChange={(p): void => {
setPage(p);
}}
/>
<div className={viewStyles.paginationPageSize}>
<ComboboxSimple
value={limit?.toString()}
defaultValue="10"
onChange={(value): void => setLimit(+value)}
items={paginationPageSizeItems}
/>
</div>
{suffixPaginationContent}
</div>
)}
</TooltipProvider>
</TanStackTableStateProvider>
</div>
);
}
const TanStackTableForward = forwardRef(TanStackTableInner) as <TData>(
props: TanStackTableProps<TData> & {
ref?: React.Ref<TanStackTableHandle>;
},
) => JSX.Element;
export const TanStackTableBase = memo(
TanStackTableForward,
) as typeof TanStackTableForward;

View File

@@ -1,21 +0,0 @@
.headerSkeleton {
width: 60% !important;
min-width: 50px !important;
height: 16px !important;
:global(.ant-skeleton-input) {
min-width: 50px !important;
height: 16px !important;
}
}
.cellSkeleton {
width: 80% !important;
min-width: 40px !important;
height: 14px !important;
:global(.ant-skeleton-input) {
min-width: 40px !important;
height: 14px !important;
}
}

View File

@@ -1,72 +0,0 @@
import { useMemo } from 'react';
import type { ColumnSizingState } from '@tanstack/react-table';
import { Skeleton } from 'antd';
import { TableColumnDef } from './types';
import { getColumnWidthStyle } from './utils';
import tableStyles from './TanStackTable.module.scss';
import styles from './TanStackTableSkeleton.module.scss';
type TanStackTableSkeletonProps<TData> = {
columns: TableColumnDef<TData>[];
rowCount: number;
isDarkMode: boolean;
columnSizing?: ColumnSizingState;
};
export function TanStackTableSkeleton<TData>({
columns,
rowCount,
isDarkMode,
columnSizing,
}: TanStackTableSkeletonProps<TData>): JSX.Element {
const rows = useMemo(() => Array.from({ length: rowCount }, (_, i) => i), [
rowCount,
]);
return (
<table className={tableStyles.tanStackTable}>
<colgroup>
{columns.map((column, index) => (
<col
key={column.id}
style={getColumnWidthStyle(
column,
columnSizing?.[column.id],
index === columns.length - 1,
)}
/>
))}
</colgroup>
<thead>
<tr>
{columns.map((column) => (
<th
key={column.id}
className={tableStyles.tableHeaderCell}
data-dark-mode={isDarkMode}
>
{typeof column.header === 'function' ? (
<Skeleton.Input active size="small" className={styles.headerSkeleton} />
) : (
column.header
)}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((rowIndex) => (
<tr key={rowIndex} className={tableStyles.tableRow}>
{columns.map((column) => (
<td key={column.id} className={tableStyles.tableCell}>
<Skeleton.Input active size="small" className={styles.cellSkeleton} />
</td>
))}
</tr>
))}
</tbody>
</table>
);
}

View File

@@ -1,206 +0,0 @@
/* eslint-disable no-restricted-imports */
import {
createContext,
ReactNode,
useCallback,
useContext,
useEffect,
useRef,
} from 'react';
/* eslint-enable no-restricted-imports */
import { VisibilityState } from '@tanstack/react-table';
import { createStore, StoreApi, useStore } from 'zustand';
const CLEAR_HOVER_DELAY_MS = 100;
type TanStackTableState = {
hoveredRowId: string | null;
clearTimeoutId: ReturnType<typeof setTimeout> | null;
setHoveredRowId: (id: string | null) => void;
scheduleClearHover: (rowId: string) => void;
isLoading: boolean;
setIsLoading: (loading: boolean) => void;
isInfiniteScrollMode: boolean;
setIsInfiniteScrollMode: (enabled: boolean) => void;
columnVisibility: VisibilityState;
setColumnVisibility: (visibility: VisibilityState) => void;
};
const createTableStateStore = (): StoreApi<TanStackTableState> =>
createStore<TanStackTableState>((set, get) => ({
hoveredRowId: null,
clearTimeoutId: null,
setHoveredRowId: (id: string | null): void => {
const { clearTimeoutId } = get();
if (clearTimeoutId) {
clearTimeout(clearTimeoutId);
set({ clearTimeoutId: null });
}
set({ hoveredRowId: id });
},
scheduleClearHover: (rowId: string): void => {
const { clearTimeoutId } = get();
if (clearTimeoutId) {
clearTimeout(clearTimeoutId);
}
const timeoutId = setTimeout(() => {
const current = get().hoveredRowId;
if (current === rowId) {
set({ hoveredRowId: null, clearTimeoutId: null });
}
}, CLEAR_HOVER_DELAY_MS);
set({ clearTimeoutId: timeoutId });
},
isLoading: false,
setIsLoading: (loading: boolean): void => {
set({ isLoading: loading });
},
isInfiniteScrollMode: false,
setIsInfiniteScrollMode: (enabled: boolean): void => {
set({ isInfiniteScrollMode: enabled });
},
columnVisibility: {},
setColumnVisibility: (visibility: VisibilityState): void => {
set({ columnVisibility: visibility });
},
}));
type TableStateStore = StoreApi<TanStackTableState>;
const TanStackTableStateContext = createContext<TableStateStore | null>(null);
export function TanStackTableStateProvider({
children,
}: {
children: ReactNode;
}): JSX.Element {
const storeRef = useRef<TableStateStore | null>(null);
if (!storeRef.current) {
storeRef.current = createTableStateStore();
}
return (
<TanStackTableStateContext.Provider value={storeRef.current}>
{children}
</TanStackTableStateContext.Provider>
);
}
const defaultStore = createTableStateStore();
export const useIsRowHovered = (rowId: string): boolean => {
const store = useContext(TanStackTableStateContext);
const isHovered = useStore(
store ?? defaultStore,
(s) => s.hoveredRowId === rowId,
);
return store ? isHovered : false;
};
export const useSetRowHovered = (rowId: string): (() => void) => {
const store = useContext(TanStackTableStateContext);
return useCallback(() => {
if (store) {
const current = store.getState().hoveredRowId;
if (current !== rowId) {
store.getState().setHoveredRowId(rowId);
}
}
}, [store, rowId]);
};
export const useClearRowHovered = (rowId: string): (() => void) => {
const store = useContext(TanStackTableStateContext);
return useCallback(() => {
if (store) {
store.getState().scheduleClearHover(rowId);
}
}, [store, rowId]);
};
export const useIsTableLoading = (): boolean => {
const store = useContext(TanStackTableStateContext);
return useStore(store ?? defaultStore, (s) => s.isLoading);
};
export const useSetTableLoading = (): ((loading: boolean) => void) => {
const store = useContext(TanStackTableStateContext);
return useCallback(
(loading: boolean) => {
if (store) {
store.getState().setIsLoading(loading);
}
},
[store],
);
};
export function TableLoadingSync({
isLoading,
isInfiniteScrollMode,
}: {
isLoading: boolean;
isInfiniteScrollMode: boolean;
}): null {
const store = useContext(TanStackTableStateContext);
// Sync on mount and when props change
useEffect(() => {
if (store) {
store.getState().setIsLoading(isLoading);
store.getState().setIsInfiniteScrollMode(isInfiniteScrollMode);
}
}, [isLoading, isInfiniteScrollMode, store]);
return null;
}
export const useShouldShowCellSkeleton = (): boolean => {
const store = useContext(TanStackTableStateContext);
return useStore(
store ?? defaultStore,
(s) => s.isLoading && !s.isInfiniteScrollMode,
);
};
export const useColumnVisibility = (): VisibilityState => {
const store = useContext(TanStackTableStateContext);
return useStore(store ?? defaultStore, (s) => s.columnVisibility);
};
export const useIsColumnVisible = (columnId: string): boolean => {
const store = useContext(TanStackTableStateContext);
return useStore(
store ?? defaultStore,
(s) => s.columnVisibility[columnId] !== false,
);
};
export const useSetColumnVisibility = (): ((
visibility: VisibilityState,
) => void) => {
const store = useContext(TanStackTableStateContext);
return useCallback(
(visibility: VisibilityState) => {
if (store) {
store.getState().setColumnVisibility(visibility);
}
},
[store],
);
};
export function ColumnVisibilitySync({
visibility,
}: {
visibility: VisibilityState;
}): null {
const setVisibility = useSetColumnVisibility();
useEffect(() => {
setVisibility(visibility);
}, [visibility, setVisibility]);
return null;
}
export default TanStackTableStateContext;

View File

@@ -1,42 +0,0 @@
import type { HTMLAttributes, ReactNode } from 'react';
import cx from 'classnames';
import tableStyles from './TanStackTable.module.scss';
type BaseProps = Omit<
HTMLAttributes<HTMLSpanElement>,
'children' | 'dangerouslySetInnerHTML'
> & {
className?: string;
};
type WithChildren = BaseProps & {
children: ReactNode;
dangerouslySetInnerHTML?: never;
};
type WithDangerousHtml = BaseProps & {
children?: never;
dangerouslySetInnerHTML: { __html: string };
};
export type TanStackTableTextProps = WithChildren | WithDangerousHtml;
function TanStackTableText({
children,
className,
dangerouslySetInnerHTML,
...rest
}: TanStackTableTextProps): JSX.Element {
return (
<span
className={cx(tableStyles.tableCellText, className)}
dangerouslySetInnerHTML={dangerouslySetInnerHTML}
{...rest}
>
{children}
</span>
);
}
export default TanStackTableText;

View File

@@ -1,152 +0,0 @@
.tanstackTableViewWrapper {
display: flex;
flex-direction: column;
flex: 1;
width: 100%;
position: relative;
min-height: 0;
}
.tanstackFixedCol {
width: 32px;
min-width: 32px;
max-width: 32px;
}
.tanstackFillerCol {
width: 100%;
min-width: 0;
}
.tanstackActionsCol {
width: 0;
min-width: 0;
max-width: 0;
}
.tanstackLoadMoreContainer {
width: 100%;
min-height: 56px;
display: flex;
align-items: center;
justify-content: center;
padding: 8px 0 12px;
flex-shrink: 0;
}
.tanstackTableVirtuoso {
width: 100%;
overflow-x: auto;
}
.tanstackTableFootLoaderCell {
text-align: center;
padding: 8px 0;
}
.tanstackTableVirtuosoScroll {
flex: 1;
width: 100%;
overflow-x: auto;
scrollbar-width: thin;
scrollbar-color: var(--bg-slate-300) transparent;
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
&::-webkit-scrollbar-corner {
background: transparent;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--bg-slate-300);
border-radius: 9999px;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-slate-200);
}
&.cellTypographySmall {
--tanstack-plain-cell-font-size: 11px;
--tanstack-plain-cell-line-height: 16px;
:global(table tr td),
:global(table thead th) {
font-size: 11px;
line-height: 16px;
letter-spacing: -0.07px;
}
}
&.cellTypographyMedium {
--tanstack-plain-cell-font-size: 13px;
--tanstack-plain-cell-line-height: 20px;
:global(table tr td),
:global(table thead th) {
font-size: 13px;
line-height: 20px;
letter-spacing: -0.07px;
}
}
&.cellTypographyLarge {
--tanstack-plain-cell-font-size: 14px;
--tanstack-plain-cell-line-height: 24px;
:global(table tr td),
:global(table thead th) {
font-size: 14px;
line-height: 24px;
letter-spacing: -0.07px;
}
}
}
.paginationContainer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
margin-top: 12px;
}
.paginationPageSize {
width: 80px;
--combobox-trigger-height: 2rem;
}
.tanstackLoadingOverlay {
position: absolute;
left: 50%;
bottom: 2rem;
transform: translateX(-50%);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 3;
border-radius: 8px;
padding: 8px 16px;
background: var(--l1-background);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
:global(.lightMode) .tanstackTableVirtuosoScroll {
scrollbar-color: var(--bg-vanilla-300) transparent;
&::-webkit-scrollbar-thumb {
background: var(--bg-vanilla-300);
}
&::-webkit-scrollbar-thumb:hover {
background: var(--bg-vanilla-100);
}
}

View File

@@ -1,35 +0,0 @@
import type { Table } from '@tanstack/react-table';
import type { TableColumnDef } from './types';
import { getColumnWidthStyle } from './utils';
export function VirtuosoTableColGroup<TData>({
columns,
table,
}: {
columns: TableColumnDef<TData>[];
table: Table<TData>;
}): JSX.Element {
const visibleTanstackColumns = table.getVisibleFlatColumns();
const columnDefsById = new Map(columns.map((c) => [c.id, c]));
const columnSizing = table.getState().columnSizing;
return (
<colgroup>
{visibleTanstackColumns.map((tanstackCol, index) => {
const colDef = columnDefsById.get(tanstackCol.id);
if (!colDef) {
return <col key={tanstackCol.id} />;
}
const persistedWidth = columnSizing[tanstackCol.id];
const isLastColumn = index === visibleTanstackColumns.length - 1;
return (
<col
key={tanstackCol.id}
style={getColumnWidthStyle(colDef, persistedWidth, isLastColumn)}
/>
);
})}
</colgroup>
);
}

View File

@@ -1,253 +0,0 @@
jest.mock('../TanStackTable.module.scss', () => ({
__esModule: true,
default: {
tableRow: 'tableRow',
tableRowActive: 'tableRowActive',
tableRowExpansion: 'tableRowExpansion',
},
}));
jest.mock('../TanStackRow', () => ({
__esModule: true,
default: (): JSX.Element => (
<td data-testid="mocked-row-cells">mocked cells</td>
),
}));
const mockSetRowHovered = jest.fn();
const mockClearRowHovered = jest.fn();
jest.mock('../TanStackTableStateContext', () => ({
useSetRowHovered: (_rowId: string): (() => void) => mockSetRowHovered,
useClearRowHovered: (_rowId: string): (() => void) => mockClearRowHovered,
}));
import { fireEvent, render, screen } from '@testing-library/react';
import TanStackCustomTableRow from '../TanStackCustomTableRow';
import type { FlatItem, TableRowContext } from '../types';
const makeItem = (id: string): FlatItem<{ id: string }> => ({
kind: 'row',
row: { original: { id }, id } as never,
});
const virtuosoAttrs = {
'data-index': 0,
'data-item-index': 0,
'data-known-size': 40,
} as const;
const baseContext: TableRowContext<{ id: string }> = {
colCount: 1,
hasSingleColumn: false,
columnOrderKey: 'col1',
columnVisibilityKey: 'col1',
};
describe('TanStackCustomTableRow', () => {
beforeEach(() => {
mockSetRowHovered.mockClear();
mockClearRowHovered.mockClear();
});
it('renders cells via TanStackRowCells', async () => {
render(
<table>
<tbody>
<TanStackCustomTableRow
{...virtuosoAttrs}
item={makeItem('1')}
context={baseContext}
/>
</tbody>
</table>,
);
expect(await screen.findByTestId('mocked-row-cells')).toBeInTheDocument();
});
it('applies active class when isRowActive returns true', () => {
const ctx: TableRowContext<{ id: string }> = {
...baseContext,
isRowActive: (row) => row.id === '1',
};
const { container } = render(
<table>
<tbody>
<TanStackCustomTableRow
{...virtuosoAttrs}
item={makeItem('1')}
context={ctx}
/>
</tbody>
</table>,
);
expect(container.querySelector('tr')).toHaveClass('tableRowActive');
});
it('does not apply active class when isRowActive returns false', () => {
const ctx: TableRowContext<{ id: string }> = {
...baseContext,
isRowActive: (row) => row.id === 'other',
};
const { container } = render(
<table>
<tbody>
<TanStackCustomTableRow
{...virtuosoAttrs}
item={makeItem('1')}
context={ctx}
/>
</tbody>
</table>,
);
expect(container.querySelector('tr')).not.toHaveClass('tableRowActive');
});
it('renders expansion row with expansion class', () => {
const item: FlatItem<{ id: string }> = {
kind: 'expansion',
row: { original: { id: '1' }, id: '1' } as never,
};
const { container } = render(
<table>
<tbody>
<TanStackCustomTableRow
{...virtuosoAttrs}
item={item}
context={baseContext}
/>
</tbody>
</table>,
);
expect(container.querySelector('tr')).toHaveClass('tableRowExpansion');
});
describe('hover state management', () => {
it('calls setRowHovered on mouse enter', () => {
const { container } = render(
<table>
<tbody>
<TanStackCustomTableRow
{...virtuosoAttrs}
item={makeItem('1')}
context={baseContext}
/>
</tbody>
</table>,
);
const row = container.querySelector('tr')!;
fireEvent.mouseEnter(row);
expect(mockSetRowHovered).toHaveBeenCalled();
});
it('calls clearRowHovered on mouse leave', () => {
const { container } = render(
<table>
<tbody>
<TanStackCustomTableRow
{...virtuosoAttrs}
item={makeItem('1')}
context={baseContext}
/>
</tbody>
</table>,
);
const row = container.querySelector('tr')!;
fireEvent.mouseLeave(row);
expect(mockClearRowHovered).toHaveBeenCalled();
});
});
describe('virtuoso integration', () => {
it('forwards data-index attribute to tr element', () => {
const { container } = render(
<table>
<tbody>
<TanStackCustomTableRow
{...virtuosoAttrs}
item={makeItem('1')}
context={baseContext}
/>
</tbody>
</table>,
);
const row = container.querySelector('tr')!;
expect(row).toHaveAttribute('data-index', '0');
});
it('forwards data-item-index attribute to tr element', () => {
const { container } = render(
<table>
<tbody>
<TanStackCustomTableRow
{...virtuosoAttrs}
item={makeItem('1')}
context={baseContext}
/>
</tbody>
</table>,
);
const row = container.querySelector('tr')!;
expect(row).toHaveAttribute('data-item-index', '0');
});
it('forwards data-known-size attribute to tr element', () => {
const { container } = render(
<table>
<tbody>
<TanStackCustomTableRow
{...virtuosoAttrs}
item={makeItem('1')}
context={baseContext}
/>
</tbody>
</table>,
);
const row = container.querySelector('tr')!;
expect(row).toHaveAttribute('data-known-size', '40');
});
});
describe('row interaction', () => {
it('applies custom style from getRowStyle in context', () => {
const ctx: TableRowContext<{ id: string }> = {
...baseContext,
getRowStyle: () => ({ backgroundColor: 'red' }),
};
const { container } = render(
<table>
<tbody>
<TanStackCustomTableRow
{...virtuosoAttrs}
item={makeItem('1')}
context={ctx}
/>
</tbody>
</table>,
);
const row = container.querySelector('tr')!;
expect(row).toHaveStyle({ backgroundColor: 'red' });
});
it('applies custom className from getRowClassName in context', () => {
const ctx: TableRowContext<{ id: string }> = {
...baseContext,
getRowClassName: () => 'custom-row-class',
};
const { container } = render(
<table>
<tbody>
<TanStackCustomTableRow
{...virtuosoAttrs}
item={makeItem('1')}
context={ctx}
/>
</tbody>
</table>,
);
const row = container.querySelector('tr')!;
expect(row).toHaveClass('custom-row-class');
});
});
});

View File

@@ -1,368 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TanStackHeaderRow from '../TanStackHeaderRow';
import type { TableColumnDef } from '../types';
jest.mock('@dnd-kit/sortable', () => ({
useSortable: (): any => ({
attributes: {},
listeners: {},
setNodeRef: jest.fn(),
setActivatorNodeRef: jest.fn(),
transform: null,
transition: null,
isDragging: false,
}),
}));
const col = (
id: string,
overrides?: Partial<TableColumnDef<unknown>>,
): TableColumnDef<unknown> => ({
id,
header: id,
cell: (): null => null,
...overrides,
});
const header = {
id: 'col',
column: {
getCanResize: () => true,
getIsResizing: () => false,
columnDef: { header: 'col' },
},
getResizeHandler: () => jest.fn(),
getContext: () => ({}),
} as never;
describe('TanStackHeaderRow', () => {
it('renders column title', () => {
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={col('timestamp', { header: 'timestamp' })}
header={header}
isDarkMode={false}
hasSingleColumn={false}
/>
</tr>
</thead>
</table>,
);
expect(screen.getByTitle('Timestamp')).toBeInTheDocument();
});
it('shows grip icon when enableMove is not false and pin is not set', () => {
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={col('body')}
header={header}
isDarkMode={false}
hasSingleColumn={false}
/>
</tr>
</thead>
</table>,
);
expect(
screen.getByRole('button', { name: /drag body/i }),
).toBeInTheDocument();
});
it('does NOT show grip icon when pin is set', () => {
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={col('indicator', { pin: 'left' })}
header={header}
isDarkMode={false}
hasSingleColumn={false}
/>
</tr>
</thead>
</table>,
);
expect(
screen.queryByRole('button', { name: /drag/i }),
).not.toBeInTheDocument();
});
it('shows remove button when enableRemove and canRemoveColumn are true', async () => {
const user = userEvent.setup();
const onRemoveColumn = jest.fn();
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={col('name', { enableRemove: true })}
header={header}
isDarkMode={false}
hasSingleColumn={false}
canRemoveColumn
onRemoveColumn={onRemoveColumn}
/>
</tr>
</thead>
</table>,
);
await user.click(screen.getByRole('button', { name: /column actions/i }));
await user.click(await screen.findByText(/remove column/i));
expect(onRemoveColumn).toHaveBeenCalledWith('name');
});
it('does NOT show remove button when enableRemove is absent', () => {
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={col('name')}
header={header}
isDarkMode={false}
hasSingleColumn={false}
canRemoveColumn
onRemoveColumn={jest.fn()}
/>
</tr>
</thead>
</table>,
);
expect(
screen.queryByRole('button', { name: /column actions/i }),
).not.toBeInTheDocument();
});
describe('sorting', () => {
const sortableCol = col('sortable', { enableSort: true, header: 'Sortable' });
const sortableHeader = {
id: 'sortable',
column: {
id: 'sortable',
getCanResize: (): boolean => true,
getIsResizing: (): boolean => false,
columnDef: { header: 'Sortable', enableSort: true },
},
getResizeHandler: (): jest.Mock => jest.fn(),
getContext: (): Record<string, unknown> => ({}),
} as never;
it('calls onSort with asc when clicking unsorted column', async () => {
const user = userEvent.setup();
const onSort = jest.fn();
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={sortableCol}
header={sortableHeader}
isDarkMode={false}
hasSingleColumn={false}
onSort={onSort}
/>
</tr>
</thead>
</table>,
);
// Sort button uses the column header as title
const sortButton = screen.getByTitle('Sortable');
await user.click(sortButton);
expect(onSort).toHaveBeenCalledWith({
columnName: 'sortable',
order: 'asc',
});
});
it('calls onSort with desc when clicking asc-sorted column', async () => {
const user = userEvent.setup();
const onSort = jest.fn();
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={sortableCol}
header={sortableHeader}
isDarkMode={false}
hasSingleColumn={false}
onSort={onSort}
orderBy={{ columnName: 'sortable', order: 'asc' }}
/>
</tr>
</thead>
</table>,
);
const sortButton = screen.getByTitle('Sortable');
await user.click(sortButton);
expect(onSort).toHaveBeenCalledWith({
columnName: 'sortable',
order: 'desc',
});
});
it('calls onSort with null when clicking desc-sorted column', async () => {
const user = userEvent.setup();
const onSort = jest.fn();
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={sortableCol}
header={sortableHeader}
isDarkMode={false}
hasSingleColumn={false}
onSort={onSort}
orderBy={{ columnName: 'sortable', order: 'desc' }}
/>
</tr>
</thead>
</table>,
);
const sortButton = screen.getByTitle('Sortable');
await user.click(sortButton);
expect(onSort).toHaveBeenCalledWith(null);
});
it('shows ascending indicator when orderBy matches column with asc', () => {
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={sortableCol}
header={sortableHeader}
isDarkMode={false}
hasSingleColumn={false}
onSort={jest.fn()}
orderBy={{ columnName: 'sortable', order: 'asc' }}
/>
</tr>
</thead>
</table>,
);
const sortButton = screen.getByTitle('Sortable');
expect(sortButton).toHaveAttribute('data-sort', 'ascending');
});
it('shows descending indicator when orderBy matches column with desc', () => {
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={sortableCol}
header={sortableHeader}
isDarkMode={false}
hasSingleColumn={false}
onSort={jest.fn()}
orderBy={{ columnName: 'sortable', order: 'desc' }}
/>
</tr>
</thead>
</table>,
);
const sortButton = screen.getByTitle('Sortable');
expect(sortButton).toHaveAttribute('data-sort', 'descending');
});
it('does not show sort button when enableSort is false', () => {
const nonSortableCol = col('nonsort', {
enableSort: false,
header: 'Nonsort',
});
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={nonSortableCol}
header={header}
isDarkMode={false}
hasSingleColumn={false}
/>
</tr>
</thead>
</table>,
);
// When enableSort is false, the header text is rendered as a span, not a button
// The title 'Nonsort' exists on the span, not on a button
const titleElement = screen.getByTitle('Nonsort');
expect(titleElement.tagName.toLowerCase()).not.toBe('button');
});
});
describe('resizing', () => {
it('shows resize handle when enableResize is not false', () => {
const resizableCol = col('resizable', { enableResize: true });
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={resizableCol}
header={header}
isDarkMode={false}
hasSingleColumn={false}
/>
</tr>
</thead>
</table>,
);
// Resize handle has title "Drag to resize column"
expect(screen.getByTitle('Drag to resize column')).toBeInTheDocument();
});
it('hides resize handle when enableResize is false', () => {
const nonResizableCol = col('noresize', { enableResize: false });
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={nonResizableCol}
header={header}
isDarkMode={false}
hasSingleColumn={false}
/>
</tr>
</thead>
</table>,
);
expect(screen.queryByTitle('Drag to resize column')).not.toBeInTheDocument();
});
});
describe('column movement', () => {
it('does not show grip when enableMove is false', () => {
const noMoveCol = col('nomove', { enableMove: false });
render(
<table>
<thead>
<tr>
<TanStackHeaderRow
column={noMoveCol}
header={header}
isDarkMode={false}
hasSingleColumn={false}
/>
</tr>
</thead>
</table>,
);
expect(
screen.queryByRole('button', { name: /drag/i }),
).not.toBeInTheDocument();
});
});
});

View File

@@ -1,288 +0,0 @@
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TanStackRowCells from '../TanStackRow';
import type { TableRowContext } from '../types';
const flexRenderMock = jest.fn((def: unknown) =>
typeof def === 'function' ? def({}) : def,
);
jest.mock('@tanstack/react-table', () => ({
flexRender: (def: unknown, _ctx?: unknown): unknown => flexRenderMock(def),
}));
type Row = { id: string };
function buildMockRow(
cells: { id: string }[],
rowData: Row = { id: 'r1' },
): Parameters<typeof TanStackRowCells>[0]['row'] {
return {
original: rowData,
getVisibleCells: () =>
cells.map((c, i) => ({
id: `cell-${i}`,
column: {
id: c.id,
columnDef: { cell: (): string => `content-${c.id}` },
},
getContext: (): Record<string, unknown> => ({}),
getValue: (): string => `content-${c.id}`,
})),
} as never;
}
describe('TanStackRowCells', () => {
beforeEach(() => flexRenderMock.mockClear());
it('renders a cell per visible column', () => {
const row = buildMockRow([{ id: 'col-a' }, { id: 'col-b' }]);
render(
<table>
<tbody>
<tr>
<TanStackRowCells<Row>
row={row as never}
context={undefined}
itemKind="row"
hasSingleColumn={false}
columnOrderKey=""
columnVisibilityKey=""
/>
</tr>
</tbody>
</table>,
);
expect(screen.getAllByRole('cell')).toHaveLength(2);
});
it('calls onRowClick when a cell is clicked', async () => {
const user = userEvent.setup();
const onRowClick = jest.fn();
const ctx: TableRowContext<Row> = {
colCount: 1,
onRowClick,
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
};
const row = buildMockRow([{ id: 'body' }]);
render(
<table>
<tbody>
<tr>
<TanStackRowCells<Row>
row={row as never}
context={ctx}
itemKind="row"
hasSingleColumn={false}
columnOrderKey=""
columnVisibilityKey=""
/>
</tr>
</tbody>
</table>,
);
await user.click(screen.getAllByRole('cell')[0]);
// onRowClick receives (rowData, itemKey) - itemKey is empty when getRowKeyData not provided
expect(onRowClick).toHaveBeenCalledWith({ id: 'r1' }, '');
});
it('calls onRowDeactivate instead of onRowClick when row is active', async () => {
const user = userEvent.setup();
const onRowClick = jest.fn();
const onRowDeactivate = jest.fn();
const ctx: TableRowContext<Row> = {
colCount: 1,
onRowClick,
onRowDeactivate,
isRowActive: () => true,
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
};
const row = buildMockRow([{ id: 'body' }]);
render(
<table>
<tbody>
<tr>
<TanStackRowCells<Row>
row={row as never}
context={ctx}
itemKind="row"
hasSingleColumn={false}
columnOrderKey=""
columnVisibilityKey=""
/>
</tr>
</tbody>
</table>,
);
await user.click(screen.getAllByRole('cell')[0]);
expect(onRowDeactivate).toHaveBeenCalled();
expect(onRowClick).not.toHaveBeenCalled();
});
it('does not render renderRowActions before hover', () => {
const ctx: TableRowContext<Row> = {
colCount: 1,
renderRowActions: () => <button type="button">action</button>,
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
};
const row = buildMockRow([{ id: 'body' }]);
render(
<table>
<tbody>
<tr>
<TanStackRowCells<Row>
row={row as never}
context={ctx}
itemKind="row"
hasSingleColumn={false}
columnOrderKey=""
columnVisibilityKey=""
/>
</tr>
</tbody>
</table>,
);
// Row actions are not rendered until hover (useIsRowHovered returns false by default)
expect(
screen.queryByRole('button', { name: 'action' }),
).not.toBeInTheDocument();
});
it('renders expansion cell with renderExpandedRow content', async () => {
const row = {
original: { id: 'r1' },
getVisibleCells: () => [],
} as never;
const ctx: TableRowContext<Row> = {
colCount: 3,
renderExpandedRow: (r) => <div>expanded-{r.id}</div>,
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
};
render(
<table>
<tbody>
<tr>
<TanStackRowCells<Row>
row={row as never}
context={ctx}
itemKind="expansion"
hasSingleColumn={false}
columnOrderKey=""
columnVisibilityKey=""
/>
</tr>
</tbody>
</table>,
);
expect(await screen.findByText('expanded-r1')).toBeInTheDocument();
});
describe('new tab click', () => {
it('calls onRowClickNewTab on ctrl+click', () => {
const onRowClick = jest.fn();
const onRowClickNewTab = jest.fn();
const ctx: TableRowContext<Row> = {
colCount: 1,
onRowClick,
onRowClickNewTab,
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
};
const row = buildMockRow([{ id: 'body' }]);
render(
<table>
<tbody>
<tr>
<TanStackRowCells<Row>
row={row as never}
context={ctx}
itemKind="row"
hasSingleColumn={false}
columnOrderKey=""
columnVisibilityKey=""
/>
</tr>
</tbody>
</table>,
);
fireEvent.click(screen.getAllByRole('cell')[0], { ctrlKey: true });
expect(onRowClickNewTab).toHaveBeenCalledWith({ id: 'r1' }, '');
expect(onRowClick).not.toHaveBeenCalled();
});
it('calls onRowClickNewTab on meta+click (cmd)', () => {
const onRowClick = jest.fn();
const onRowClickNewTab = jest.fn();
const ctx: TableRowContext<Row> = {
colCount: 1,
onRowClick,
onRowClickNewTab,
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
};
const row = buildMockRow([{ id: 'body' }]);
render(
<table>
<tbody>
<tr>
<TanStackRowCells<Row>
row={row as never}
context={ctx}
itemKind="row"
hasSingleColumn={false}
columnOrderKey=""
columnVisibilityKey=""
/>
</tr>
</tbody>
</table>,
);
fireEvent.click(screen.getAllByRole('cell')[0], { metaKey: true });
expect(onRowClickNewTab).toHaveBeenCalledWith({ id: 'r1' }, '');
expect(onRowClick).not.toHaveBeenCalled();
});
it('does not call onRowClick when modifier key is pressed', () => {
const onRowClick = jest.fn();
const onRowClickNewTab = jest.fn();
const ctx: TableRowContext<Row> = {
colCount: 1,
onRowClick,
onRowClickNewTab,
hasSingleColumn: false,
columnOrderKey: '',
columnVisibilityKey: '',
};
const row = buildMockRow([{ id: 'body' }]);
render(
<table>
<tbody>
<tr>
<TanStackRowCells<Row>
row={row as never}
context={ctx}
itemKind="row"
hasSingleColumn={false}
columnOrderKey=""
columnVisibilityKey=""
/>
</tr>
</tbody>
</table>,
);
fireEvent.click(screen.getAllByRole('cell')[0], { ctrlKey: true });
expect(onRowClick).not.toHaveBeenCalled();
});
});
});

View File

@@ -1,440 +0,0 @@
import { fireEvent, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { UrlUpdateEvent } from 'nuqs/adapters/testing';
import { renderTanStackTable } from './testUtils';
jest.mock('hooks/useDarkMode', () => ({
useIsDarkMode: (): boolean => false,
}));
jest.mock('../TanStackTable.module.scss', () => ({
__esModule: true,
default: {
tanStackTable: 'tanStackTable',
tableRow: 'tableRow',
tableRowActive: 'tableRowActive',
tableRowExpansion: 'tableRowExpansion',
tableCell: 'tableCell',
tableCellExpansion: 'tableCellExpansion',
tableHeaderCell: 'tableHeaderCell',
tableCellText: 'tableCellText',
tableViewRowActions: 'tableViewRowActions',
},
}));
describe('TanStackTableView Integration', () => {
describe('rendering', () => {
it('renders all data rows', async () => {
renderTanStackTable({});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
expect(screen.getByText('Item 2')).toBeInTheDocument();
expect(screen.getByText('Item 3')).toBeInTheDocument();
});
});
it('renders column headers', async () => {
renderTanStackTable({});
await waitFor(() => {
expect(screen.getByText('ID')).toBeInTheDocument();
expect(screen.getByText('Name')).toBeInTheDocument();
expect(screen.getByText('Value')).toBeInTheDocument();
});
});
it('renders empty state when data is empty and not loading', async () => {
renderTanStackTable({
props: { data: [], isLoading: false },
});
// Table should still render but with no data rows
await waitFor(() => {
expect(screen.queryByText('Item 1')).not.toBeInTheDocument();
});
});
it('renders table structure when loading with no previous data', async () => {
renderTanStackTable({
props: { data: [], isLoading: true },
});
// Table should render with skeleton rows
await waitFor(() => {
expect(screen.getByRole('table')).toBeInTheDocument();
});
});
});
describe('loading states', () => {
it('keeps table mounted when loading with no data', () => {
renderTanStackTable({
props: { data: [], isLoading: true },
});
// Table should still be in the DOM for skeleton rows
expect(screen.getByRole('table')).toBeInTheDocument();
});
it('shows loading spinner for infinite scroll when loading', () => {
renderTanStackTable({
props: { isLoading: true, onEndReached: jest.fn() },
});
expect(screen.getByTestId('tanstack-infinite-loader')).toBeInTheDocument();
});
it('does not show loading spinner for infinite scroll when not loading', () => {
renderTanStackTable({
props: { isLoading: false, onEndReached: jest.fn() },
});
expect(
screen.queryByTestId('tanstack-infinite-loader'),
).not.toBeInTheDocument();
});
it('does not show loading spinner when not in infinite scroll mode', () => {
renderTanStackTable({
props: { isLoading: true },
});
expect(
screen.queryByTestId('tanstack-infinite-loader'),
).not.toBeInTheDocument();
});
});
describe('pagination', () => {
it('renders pagination when pagination prop is provided', async () => {
renderTanStackTable({
props: {
pagination: { total: 100, defaultPage: 1, defaultLimit: 10 },
},
});
await waitFor(() => {
// Look for pagination navigation or page number text
expect(screen.getByRole('navigation')).toBeInTheDocument();
});
});
it('updates page when clicking page number', async () => {
const user = userEvent.setup();
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
renderTanStackTable({
props: {
pagination: { total: 100, defaultPage: 1, defaultLimit: 10 },
enableQueryParams: true,
},
onUrlUpdate,
});
await waitFor(() => {
expect(screen.getByRole('navigation')).toBeInTheDocument();
});
// Find page 2 button/link within pagination navigation
const nav = screen.getByRole('navigation');
const page2 = Array.from(nav.querySelectorAll('button')).find(
(btn) => btn.textContent?.trim() === '2',
);
if (!page2) {
throw new Error('Page 2 button not found in pagination');
}
await user.click(page2);
await waitFor(() => {
const lastPage = onUrlUpdate.mock.calls
.map((call) => call[0].searchParams.get('page'))
.filter(Boolean)
.pop();
expect(lastPage).toBe('2');
});
});
it('does not render pagination in infinite scroll mode', async () => {
renderTanStackTable({
props: {
pagination: { total: 100 },
onEndReached: jest.fn(), // This enables infinite scroll mode
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
// Pagination should not be visible in infinite scroll mode
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
});
it('renders prefixPaginationContent before pagination', async () => {
renderTanStackTable({
props: {
pagination: { total: 100 },
prefixPaginationContent: <span data-testid="prefix-content">Prefix</span>,
},
});
await waitFor(() => {
expect(screen.getByTestId('prefix-content')).toBeInTheDocument();
});
});
it('renders suffixPaginationContent after pagination', async () => {
renderTanStackTable({
props: {
pagination: { total: 100 },
suffixPaginationContent: <span data-testid="suffix-content">Suffix</span>,
},
});
await waitFor(() => {
expect(screen.getByTestId('suffix-content')).toBeInTheDocument();
});
});
});
describe('sorting', () => {
it('updates orderBy URL param when clicking sortable header', async () => {
const user = userEvent.setup();
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
renderTanStackTable({
props: { enableQueryParams: true },
onUrlUpdate,
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
// Find the sortable column header's sort button (ID column has enableSort: true)
const sortButton = screen.getByTitle('ID');
await user.click(sortButton);
await waitFor(() => {
const lastOrderBy = onUrlUpdate.mock.calls
.map((call) => call[0].searchParams.get('order_by'))
.filter(Boolean)
.pop();
expect(lastOrderBy).toBeDefined();
const parsed = JSON.parse(lastOrderBy!);
expect(parsed.columnName).toBe('id');
expect(parsed.order).toBe('asc');
});
});
it('toggles sort order on subsequent clicks', async () => {
const user = userEvent.setup();
const onUrlUpdate = jest.fn<void, [UrlUpdateEvent]>();
renderTanStackTable({
props: { enableQueryParams: true },
onUrlUpdate,
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
const sortButton = screen.getByTitle('ID');
// First click - asc
await user.click(sortButton);
// Second click - desc
await user.click(sortButton);
await waitFor(() => {
const lastOrderBy = onUrlUpdate.mock.calls
.map((call) => call[0].searchParams.get('order_by'))
.filter(Boolean)
.pop();
if (lastOrderBy) {
const parsed = JSON.parse(lastOrderBy);
expect(parsed.order).toBe('desc');
}
});
});
});
describe('row selection', () => {
it('calls onRowClick with row data and itemKey', async () => {
const user = userEvent.setup();
const onRowClick = jest.fn();
renderTanStackTable({
props: {
onRowClick,
getRowKey: (row) => row.id,
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
await user.click(screen.getByText('Item 1'));
expect(onRowClick).toHaveBeenCalledWith(
expect.objectContaining({ id: '1', name: 'Item 1' }),
'1',
);
});
it('applies active class when isRowActive returns true', async () => {
renderTanStackTable({
props: {
isRowActive: (row) => row.id === '1',
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
// Find the row containing Item 1 and check for active class
const cell = screen.getByText('Item 1');
const row = cell.closest('tr');
expect(row).toHaveClass('tableRowActive');
});
it('calls onRowDeactivate when clicking active row', async () => {
const user = userEvent.setup();
const onRowClick = jest.fn();
const onRowDeactivate = jest.fn();
renderTanStackTable({
props: {
onRowClick,
onRowDeactivate,
isRowActive: (row) => row.id === '1',
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
await user.click(screen.getByText('Item 1'));
expect(onRowDeactivate).toHaveBeenCalled();
expect(onRowClick).not.toHaveBeenCalled();
});
it('opens in new tab on ctrl+click', async () => {
const onRowClick = jest.fn();
const onRowClickNewTab = jest.fn();
renderTanStackTable({
props: {
onRowClick,
onRowClickNewTab,
getRowKey: (row) => row.id,
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
fireEvent.click(screen.getByText('Item 1'), { ctrlKey: true });
expect(onRowClickNewTab).toHaveBeenCalledWith(
expect.objectContaining({ id: '1' }),
'1',
);
expect(onRowClick).not.toHaveBeenCalled();
});
it('opens in new tab on meta+click', async () => {
const onRowClick = jest.fn();
const onRowClickNewTab = jest.fn();
renderTanStackTable({
props: {
onRowClick,
onRowClickNewTab,
getRowKey: (row) => row.id,
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
fireEvent.click(screen.getByText('Item 1'), { metaKey: true });
expect(onRowClickNewTab).toHaveBeenCalledWith(
expect.objectContaining({ id: '1' }),
'1',
);
expect(onRowClick).not.toHaveBeenCalled();
});
});
describe('row expansion', () => {
it('renders expanded content below the row when expanded', async () => {
renderTanStackTable({
props: {
renderExpandedRow: (row) => (
<div data-testid="expanded-content">Expanded: {row.name}</div>
),
getRowCanExpand: () => true,
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
// Find and click expand button (if available in the row)
// The expansion is controlled by TanStack Table's expanded state
// For now, just verify the renderExpandedRow prop is wired correctly
// by checking the table renders without errors
expect(screen.getByRole('table')).toBeInTheDocument();
});
});
describe('infinite scroll', () => {
it('calls onEndReached when provided', async () => {
const onEndReached = jest.fn();
renderTanStackTable({
props: {
onEndReached,
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
// Virtuoso will call onEndReached based on scroll position
// In mock context, we verify the prop is wired correctly
expect(onEndReached).toBeDefined();
});
it('shows loading spinner at bottom when loading in infinite scroll mode', () => {
renderTanStackTable({
props: {
isLoading: true,
onEndReached: jest.fn(),
},
});
expect(screen.getByTestId('tanstack-infinite-loader')).toBeInTheDocument();
});
it('hides pagination in infinite scroll mode', async () => {
renderTanStackTable({
props: {
pagination: { total: 100 },
onEndReached: jest.fn(),
},
});
await waitFor(() => {
expect(screen.getByText('Item 1')).toBeInTheDocument();
});
// When onEndReached is provided, pagination should not render
expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
});
});
});

View File

@@ -1,94 +0,0 @@
import { ReactNode } from 'react';
import { VirtuosoMockContext } from 'react-virtuoso';
import { TooltipProvider } from '@signozhq/ui';
import { render, RenderResult } from '@testing-library/react';
import { NuqsTestingAdapter, OnUrlUpdateFunction } from 'nuqs/adapters/testing';
import TanStackTable from '../index';
import type { TableColumnDef, TanStackTableProps } from '../types';
// NOTE: Test files importing this utility must add this mock at the top of their file:
// jest.mock('hooks/useDarkMode', () => ({ useIsDarkMode: (): boolean => false }));
// Default test data types
export type TestRow = { id: string; name: string; value: number };
export const defaultColumns: TableColumnDef<TestRow>[] = [
{
id: 'id',
header: 'ID',
accessorKey: 'id',
enableSort: true,
cell: ({ value }): string => String(value),
},
{
id: 'name',
header: 'Name',
accessorKey: 'name',
cell: ({ value }): string => String(value),
},
{
id: 'value',
header: 'Value',
accessorKey: 'value',
enableSort: true,
cell: ({ value }): string => String(value),
},
];
export const defaultData: TestRow[] = [
{ id: '1', name: 'Item 1', value: 100 },
{ id: '2', name: 'Item 2', value: 200 },
{ id: '3', name: 'Item 3', value: 300 },
];
export type RenderTanStackTableOptions<T> = {
props?: Partial<TanStackTableProps<T>>;
queryParams?: Record<string, string>;
onUrlUpdate?: OnUrlUpdateFunction;
};
export function renderTanStackTable<T = TestRow>(
options: RenderTanStackTableOptions<T> = {},
): RenderResult {
const { props = {}, queryParams, onUrlUpdate } = options;
const mergedProps = {
data: (defaultData as unknown) as T[],
columns: (defaultColumns as unknown) as TableColumnDef<T>[],
...props,
} as TanStackTableProps<T>;
return render(
<NuqsTestingAdapter searchParams={queryParams} onUrlUpdate={onUrlUpdate}>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 500, itemHeight: 50 }}
>
<TooltipProvider>
<TanStackTable<T> {...mergedProps} />
</TooltipProvider>
</VirtuosoMockContext.Provider>
</NuqsTestingAdapter>,
);
}
// Helper to wrap any component with test providers (for unit tests)
export function renderWithProviders(
ui: ReactNode,
options: {
queryParams?: Record<string, string>;
onUrlUpdate?: OnUrlUpdateFunction;
} = {},
): RenderResult {
const { queryParams, onUrlUpdate } = options;
return render(
<NuqsTestingAdapter searchParams={queryParams} onUrlUpdate={onUrlUpdate}>
<VirtuosoMockContext.Provider
value={{ viewportHeight: 500, itemHeight: 50 }}
>
<TooltipProvider>{ui}</TooltipProvider>
</VirtuosoMockContext.Provider>
</NuqsTestingAdapter>,
);
}

View File

@@ -1,247 +0,0 @@
/* eslint-disable no-restricted-syntax */
import { act, renderHook } from '@testing-library/react';
import { TableColumnDef } from '../types';
import { useColumnState } from '../useColumnState';
import { useColumnStore } from '../useColumnStore';
const TEST_KEY = 'test-state';
type TestRow = { id: string; name: string };
const col = (
id: string,
overrides: Partial<TableColumnDef<TestRow>> = {},
): TableColumnDef<TestRow> => ({
id,
header: id,
cell: (): null => null,
...overrides,
});
describe('useColumnState', () => {
beforeEach(() => {
useColumnStore.setState({ tables: {} });
localStorage.clear();
});
describe('initialization', () => {
it('initializes store from column defaults on mount', () => {
const columns = [
col('a', { defaultVisibility: true }),
col('b', { defaultVisibility: false }),
col('c'),
];
renderHook(() => useColumnState({ storageKey: TEST_KEY, columns }));
const state = useColumnStore.getState().tables[TEST_KEY];
expect(state.hiddenColumnIds).toEqual(['b']);
});
it('does not initialize without storageKey', () => {
const columns = [col('a', { defaultVisibility: false })];
renderHook(() => useColumnState({ columns }));
expect(useColumnStore.getState().tables[TEST_KEY]).toBeUndefined();
});
});
describe('columnVisibility', () => {
it('returns visibility state from hidden columns', () => {
const columns = [col('a'), col('b'), col('c')];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
useColumnStore.getState().hideColumn(TEST_KEY, 'b');
});
const { result } = renderHook(() =>
useColumnState({ storageKey: TEST_KEY, columns }),
);
expect(result.current.columnVisibility).toEqual({ b: false });
});
it('applies visibilityBehavior for grouped state', () => {
const columns = [
col('ungrouped', { visibilityBehavior: 'hidden-on-expand' }),
col('grouped', { visibilityBehavior: 'hidden-on-collapse' }),
col('always'),
];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
});
// Not grouped
const { result: notGrouped } = renderHook(() =>
useColumnState({ storageKey: TEST_KEY, columns, isGrouped: false }),
);
expect(notGrouped.current.columnVisibility).toEqual({ grouped: false });
// Grouped
const { result: grouped } = renderHook(() =>
useColumnState({ storageKey: TEST_KEY, columns, isGrouped: true }),
);
expect(grouped.current.columnVisibility).toEqual({ ungrouped: false });
});
it('combines store hidden + visibilityBehavior', () => {
const columns = [
col('a', { visibilityBehavior: 'hidden-on-expand' }),
col('b'),
];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
useColumnStore.getState().hideColumn(TEST_KEY, 'b');
});
const { result } = renderHook(() =>
useColumnState({ storageKey: TEST_KEY, columns, isGrouped: true }),
);
expect(result.current.columnVisibility).toEqual({ a: false, b: false });
});
});
describe('sortedColumns', () => {
it('returns columns in original order when no order set', () => {
const columns = [col('a'), col('b'), col('c')];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
});
const { result } = renderHook(() =>
useColumnState({ storageKey: TEST_KEY, columns }),
);
expect(result.current.sortedColumns.map((c) => c.id)).toEqual([
'a',
'b',
'c',
]);
});
it('returns columns sorted by stored order', () => {
const columns = [col('a'), col('b'), col('c')];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
useColumnStore.getState().setColumnOrder(TEST_KEY, ['c', 'a', 'b']);
});
const { result } = renderHook(() =>
useColumnState({ storageKey: TEST_KEY, columns }),
);
expect(result.current.sortedColumns.map((c) => c.id)).toEqual([
'c',
'a',
'b',
]);
});
it('keeps pinned columns at the start', () => {
const columns = [col('a'), col('pinned', { pin: 'left' }), col('b')];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
useColumnStore.getState().setColumnOrder(TEST_KEY, ['b', 'a']);
});
const { result } = renderHook(() =>
useColumnState({ storageKey: TEST_KEY, columns }),
);
expect(result.current.sortedColumns.map((c) => c.id)).toEqual([
'pinned',
'b',
'a',
]);
});
});
describe('actions', () => {
it('hideColumn hides a column', () => {
const columns = [col('a'), col('b')];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
});
const { result } = renderHook(() =>
useColumnState({ storageKey: TEST_KEY, columns }),
);
act(() => {
result.current.hideColumn('a');
});
expect(result.current.columnVisibility).toEqual({ a: false });
});
it('showColumn shows a column', () => {
const columns = [col('a', { defaultVisibility: false })];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
});
const { result } = renderHook(() =>
useColumnState({ storageKey: TEST_KEY, columns }),
);
expect(result.current.columnVisibility).toEqual({ a: false });
act(() => {
result.current.showColumn('a');
});
expect(result.current.columnVisibility).toEqual({});
});
it('setColumnSizing updates sizing', () => {
const columns = [col('a')];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
});
const { result } = renderHook(() =>
useColumnState({ storageKey: TEST_KEY, columns }),
);
act(() => {
result.current.setColumnSizing({ a: 200 });
});
expect(result.current.columnSizing).toEqual({ a: 200 });
});
it('setColumnOrder updates order from column array', () => {
const columns = [col('a'), col('b'), col('c')];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns);
});
const { result } = renderHook(() =>
useColumnState({ storageKey: TEST_KEY, columns }),
);
act(() => {
result.current.setColumnOrder([col('c'), col('a'), col('b')]);
});
expect(result.current.sortedColumns.map((c) => c.id)).toEqual([
'c',
'a',
'b',
]);
});
});
});

View File

@@ -1,296 +0,0 @@
/* eslint-disable no-restricted-syntax */
import { act, renderHook } from '@testing-library/react';
import {
useColumnOrder,
useColumnSizing,
useColumnStore,
useHiddenColumnIds,
} from '../useColumnStore';
const TEST_KEY = 'test-table';
describe('useColumnStore', () => {
beforeEach(() => {
useColumnStore.getState().tables = {};
localStorage.clear();
});
describe('initializeFromDefaults', () => {
it('initializes hidden columns from defaultVisibility: false', () => {
const columns = [
{ id: 'a', defaultVisibility: true },
{ id: 'b', defaultVisibility: false },
{ id: 'c' }, // defaults to visible
];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns as any);
});
const state = useColumnStore.getState().tables[TEST_KEY];
expect(state.hiddenColumnIds).toEqual(['b']);
expect(state.columnOrder).toEqual([]);
expect(state.columnSizing).toEqual({});
});
it('does not reinitialize if already exists', () => {
act(() => {
useColumnStore
.getState()
.initializeFromDefaults(TEST_KEY, [
{ id: 'a', defaultVisibility: false },
] as any);
useColumnStore.getState().hideColumn(TEST_KEY, 'x');
useColumnStore
.getState()
.initializeFromDefaults(TEST_KEY, [
{ id: 'b', defaultVisibility: false },
] as any);
});
const state = useColumnStore.getState().tables[TEST_KEY];
expect(state.hiddenColumnIds).toContain('a');
expect(state.hiddenColumnIds).toContain('x');
expect(state.hiddenColumnIds).not.toContain('b');
});
});
describe('hideColumn / showColumn / toggleColumn', () => {
beforeEach(() => {
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, []);
});
});
it('hideColumn adds to hiddenColumnIds', () => {
act(() => {
useColumnStore.getState().hideColumn(TEST_KEY, 'col1');
});
expect(useColumnStore.getState().tables[TEST_KEY].hiddenColumnIds).toContain(
'col1',
);
});
it('hideColumn is idempotent', () => {
act(() => {
useColumnStore.getState().hideColumn(TEST_KEY, 'col1');
useColumnStore.getState().hideColumn(TEST_KEY, 'col1');
});
expect(
useColumnStore
.getState()
.tables[TEST_KEY].hiddenColumnIds.filter((id) => id === 'col1'),
).toHaveLength(1);
});
it('showColumn removes from hiddenColumnIds', () => {
act(() => {
useColumnStore.getState().hideColumn(TEST_KEY, 'col1');
useColumnStore.getState().showColumn(TEST_KEY, 'col1');
});
expect(
useColumnStore.getState().tables[TEST_KEY].hiddenColumnIds,
).not.toContain('col1');
});
it('toggleColumn toggles visibility', () => {
act(() => {
useColumnStore.getState().toggleColumn(TEST_KEY, 'col1');
});
expect(useColumnStore.getState().tables[TEST_KEY].hiddenColumnIds).toContain(
'col1',
);
act(() => {
useColumnStore.getState().toggleColumn(TEST_KEY, 'col1');
});
expect(
useColumnStore.getState().tables[TEST_KEY].hiddenColumnIds,
).not.toContain('col1');
});
});
describe('setColumnSizing', () => {
beforeEach(() => {
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, []);
});
});
it('updates column sizing', () => {
act(() => {
useColumnStore
.getState()
.setColumnSizing(TEST_KEY, { col1: 200, col2: 300 });
});
expect(useColumnStore.getState().tables[TEST_KEY].columnSizing).toEqual({
col1: 200,
col2: 300,
});
});
});
describe('setColumnOrder', () => {
beforeEach(() => {
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, []);
});
});
it('updates column order', () => {
act(() => {
useColumnStore
.getState()
.setColumnOrder(TEST_KEY, ['col2', 'col1', 'col3']);
});
expect(useColumnStore.getState().tables[TEST_KEY].columnOrder).toEqual([
'col2',
'col1',
'col3',
]);
});
});
describe('resetToDefaults', () => {
it('resets to column defaults', () => {
const columns = [
{ id: 'a', defaultVisibility: false },
{ id: 'b', defaultVisibility: true },
];
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, columns as any);
useColumnStore.getState().showColumn(TEST_KEY, 'a');
useColumnStore.getState().hideColumn(TEST_KEY, 'b');
useColumnStore.getState().setColumnOrder(TEST_KEY, ['b', 'a']);
useColumnStore.getState().setColumnSizing(TEST_KEY, { a: 100 });
});
act(() => {
useColumnStore.getState().resetToDefaults(TEST_KEY, columns as any);
});
const state = useColumnStore.getState().tables[TEST_KEY];
expect(state.hiddenColumnIds).toEqual(['a']);
expect(state.columnOrder).toEqual([]);
expect(state.columnSizing).toEqual({});
});
});
describe('cleanupStaleHiddenColumns', () => {
it('removes hidden column IDs that are not in validColumnIds', () => {
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, []);
useColumnStore.getState().hideColumn(TEST_KEY, 'col1');
useColumnStore.getState().hideColumn(TEST_KEY, 'col2');
useColumnStore.getState().hideColumn(TEST_KEY, 'col3');
});
// Only col1 and col3 are valid now
act(() => {
useColumnStore
.getState()
.cleanupStaleHiddenColumns(TEST_KEY, new Set(['col1', 'col3']));
});
const state = useColumnStore.getState().tables[TEST_KEY];
expect(state.hiddenColumnIds).toEqual(['col1', 'col3']);
expect(state.hiddenColumnIds).not.toContain('col2');
});
it('does nothing when all hidden columns are valid', () => {
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, []);
useColumnStore.getState().hideColumn(TEST_KEY, 'col1');
useColumnStore.getState().hideColumn(TEST_KEY, 'col2');
});
const stateBefore = useColumnStore.getState().tables[TEST_KEY];
const hiddenBefore = [...stateBefore.hiddenColumnIds];
act(() => {
useColumnStore
.getState()
.cleanupStaleHiddenColumns(TEST_KEY, new Set(['col1', 'col2', 'col3']));
});
const stateAfter = useColumnStore.getState().tables[TEST_KEY];
expect(stateAfter.hiddenColumnIds).toEqual(hiddenBefore);
});
it('does nothing for unknown storage key', () => {
act(() => {
useColumnStore
.getState()
.cleanupStaleHiddenColumns('unknown-key', new Set(['col1']));
});
// Should not throw or create state
expect(useColumnStore.getState().tables['unknown-key']).toBeUndefined();
});
});
describe('selector hooks', () => {
it('useHiddenColumnIds returns hidden columns', () => {
act(() => {
useColumnStore
.getState()
.initializeFromDefaults(TEST_KEY, [
{ id: 'a', defaultVisibility: false },
] as any);
});
const { result } = renderHook(() => useHiddenColumnIds(TEST_KEY));
expect(result.current).toEqual(['a']);
});
it('useHiddenColumnIds returns a stable snapshot for persisted state', () => {
localStorage.setItem(
'@signoz/table-columns/test-table',
JSON.stringify({
hiddenColumnIds: ['persisted'],
columnOrder: [],
columnSizing: {},
}),
);
const { result, rerender } = renderHook(() => useHiddenColumnIds(TEST_KEY));
const firstSnapshot = result.current;
rerender();
expect(result.current).toBe(firstSnapshot);
});
it('useColumnSizing returns sizing', () => {
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, []);
useColumnStore.getState().setColumnSizing(TEST_KEY, { col1: 150 });
});
const { result } = renderHook(() => useColumnSizing(TEST_KEY));
expect(result.current).toEqual({ col1: 150 });
});
it('useColumnOrder returns order', () => {
act(() => {
useColumnStore.getState().initializeFromDefaults(TEST_KEY, []);
useColumnStore.getState().setColumnOrder(TEST_KEY, ['c', 'b', 'a']);
});
const { result } = renderHook(() => useColumnOrder(TEST_KEY));
expect(result.current).toEqual(['c', 'b', 'a']);
});
it('returns empty defaults for unknown storageKey', () => {
const { result: hidden } = renderHook(() => useHiddenColumnIds('unknown'));
const { result: sizing } = renderHook(() => useColumnSizing('unknown'));
const { result: order } = renderHook(() => useColumnOrder('unknown'));
expect(hidden.current).toEqual([]);
expect(sizing.current).toEqual({});
expect(order.current).toEqual([]);
});
});
});

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