mirror of
https://github.com/SigNoz/signoz.git
synced 2026-04-24 12:50:28 +01:00
Compare commits
7 Commits
e2e/alert_
...
chore/auto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6d11a4da9 | ||
|
|
156459e414 | ||
|
|
85a38d5608 | ||
|
|
04552fa2e9 | ||
|
|
535ee9900c | ||
|
|
07e7fcac4b | ||
|
|
c595506a09 |
@@ -408,6 +408,16 @@ cloudintegration:
|
||||
# The version of the cloud integration agent.
|
||||
version: v0.0.8
|
||||
|
||||
##################### Trace Detail #####################
|
||||
tracedetail:
|
||||
waterfall:
|
||||
# Number of spans returned per request when the trace is too large to show all at once.
|
||||
span_page_size: 500
|
||||
# Maximum depth of descendents to auto-expand for the selected span.
|
||||
max_depth_to_auto_expand: 5
|
||||
# Threshold below which all spans are returned without windowing.
|
||||
max_limit_to_select_all_spans: 10000
|
||||
|
||||
##################### Authz #################################
|
||||
authz:
|
||||
# Specifies the authz provider to use.
|
||||
|
||||
@@ -4581,6 +4581,140 @@ components:
|
||||
TimeDuration:
|
||||
format: int64
|
||||
type: integer
|
||||
TracedetailtypesEvent:
|
||||
properties:
|
||||
attributeMap:
|
||||
additionalProperties: {}
|
||||
type: object
|
||||
isError:
|
||||
type: boolean
|
||||
name:
|
||||
type: string
|
||||
timeUnixNano:
|
||||
minimum: 0
|
||||
type: integer
|
||||
type: object
|
||||
TracedetailtypesGettableWaterfallTrace:
|
||||
properties:
|
||||
endTimestampMillis:
|
||||
minimum: 0
|
||||
type: integer
|
||||
hasMissingSpans:
|
||||
type: boolean
|
||||
hasMore:
|
||||
type: boolean
|
||||
rootServiceEntryPoint:
|
||||
type: string
|
||||
rootServiceName:
|
||||
type: string
|
||||
serviceNameToTotalDurationMap:
|
||||
additionalProperties:
|
||||
minimum: 0
|
||||
type: integer
|
||||
nullable: true
|
||||
type: object
|
||||
spans:
|
||||
items:
|
||||
$ref: '#/components/schemas/TracedetailtypesWaterfallSpan'
|
||||
nullable: true
|
||||
type: array
|
||||
startTimestampMillis:
|
||||
minimum: 0
|
||||
type: integer
|
||||
totalErrorSpansCount:
|
||||
minimum: 0
|
||||
type: integer
|
||||
totalSpansCount:
|
||||
minimum: 0
|
||||
type: integer
|
||||
uncollapsedSpans:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
TracedetailtypesPostableWaterfall:
|
||||
properties:
|
||||
limit:
|
||||
minimum: 0
|
||||
type: integer
|
||||
selectedSpanId:
|
||||
type: string
|
||||
uncollapsedSpans:
|
||||
items:
|
||||
type: string
|
||||
nullable: true
|
||||
type: array
|
||||
type: object
|
||||
TracedetailtypesWaterfallSpan:
|
||||
properties:
|
||||
attributes:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
db_name:
|
||||
type: string
|
||||
db_operation:
|
||||
type: string
|
||||
duration_nano:
|
||||
minimum: 0
|
||||
type: integer
|
||||
events:
|
||||
items:
|
||||
$ref: '#/components/schemas/TracedetailtypesEvent'
|
||||
nullable: true
|
||||
type: array
|
||||
external_http_method:
|
||||
type: string
|
||||
external_http_url:
|
||||
type: string
|
||||
flags:
|
||||
minimum: 0
|
||||
type: integer
|
||||
has_children:
|
||||
type: boolean
|
||||
has_error:
|
||||
type: boolean
|
||||
http_host:
|
||||
type: string
|
||||
http_method:
|
||||
type: string
|
||||
http_url:
|
||||
type: string
|
||||
is_remote:
|
||||
type: string
|
||||
kind_string:
|
||||
type: string
|
||||
level:
|
||||
minimum: 0
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
parent_span_id:
|
||||
type: string
|
||||
resource:
|
||||
additionalProperties:
|
||||
type: string
|
||||
nullable: true
|
||||
type: object
|
||||
response_status_code:
|
||||
type: string
|
||||
span_id:
|
||||
type: string
|
||||
status_code:
|
||||
type: integer
|
||||
status_code_string:
|
||||
type: string
|
||||
status_message:
|
||||
type: string
|
||||
sub_tree_node_count:
|
||||
minimum: 0
|
||||
type: integer
|
||||
trace_id:
|
||||
type: string
|
||||
trace_state:
|
||||
type: string
|
||||
type: object
|
||||
TypesAlertStatus:
|
||||
properties:
|
||||
inhibitedBy:
|
||||
@@ -15995,6 +16129,76 @@ paths:
|
||||
summary: Put profile in Zeus for a deployment.
|
||||
tags:
|
||||
- zeus
|
||||
/api/v3/traces/{traceID}/waterfall:
|
||||
post:
|
||||
deprecated: false
|
||||
description: Returns the waterfall view of spans for a given trace ID with tree
|
||||
structure, metadata, and windowed pagination
|
||||
operationId: GetWaterfall
|
||||
parameters:
|
||||
- in: path
|
||||
name: traceID
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TracedetailtypesPostableWaterfall'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/TracedetailtypesGettableWaterfallTrace'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Get waterfall view for a trace
|
||||
tags:
|
||||
- tracedetail
|
||||
/api/v5/query_range:
|
||||
post:
|
||||
deprecated: false
|
||||
|
||||
@@ -86,6 +86,28 @@ func (provider *provider) BatchCheck(ctx context.Context, tupleReq map[string]*o
|
||||
return provider.openfgaServer.BatchCheck(ctx, tupleReq)
|
||||
}
|
||||
|
||||
func (provider *provider) CheckTransactions(ctx context.Context, subject string, orgID valuer.UUID, transactions []*authtypes.Transaction) ([]*authtypes.TransactionWithAuthorization, error) {
|
||||
tuples, err := authtypes.NewTuplesFromTransactions(transactions, subject, orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
batchResults, err := provider.openfgaServer.BatchCheck(ctx, tuples)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results := make([]*authtypes.TransactionWithAuthorization, len(transactions))
|
||||
for i, txn := range transactions {
|
||||
result := batchResults[txn.ID.StringValue()]
|
||||
results[i] = &authtypes.TransactionWithAuthorization{
|
||||
Transaction: txn,
|
||||
Authorized: result.Authorized,
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (provider *provider) ListObjects(ctx context.Context, subject string, relation authtypes.Relation, objectType authtypes.Type) ([]*authtypes.Object, error) {
|
||||
return provider.openfgaServer.ListObjects(ctx, subject, relation, objectType)
|
||||
}
|
||||
|
||||
@@ -286,6 +286,20 @@
|
||||
// Prevents useStore.getState() - export standalone actions instead
|
||||
"signoz/no-navigator-clipboard": "error",
|
||||
// Prevents navigator.clipboard - use useCopyToClipboard hook instead (disabled in tests via override)
|
||||
"signoz/no-raw-absolute-path": "error",
|
||||
// Prevents window.open(path), window.location.origin + path, window.location.href = path
|
||||
"no-restricted-globals": [
|
||||
"error",
|
||||
{
|
||||
"name": "localStorage",
|
||||
"message": "Use scoped wrappers from api/browser/localstorage/ instead (ensures keys are prefixed when served under a URL base path)."
|
||||
},
|
||||
{
|
||||
"name": "sessionStorage",
|
||||
"message": "Use scoped wrappers from api/browser/sessionstorage/ instead (ensures keys are prefixed when served under a URL base path)."
|
||||
}
|
||||
],
|
||||
// Prevents direct localStorage/sessionStorage access — use scoped wrappers
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
@@ -597,8 +611,11 @@
|
||||
"rules": {
|
||||
"import/first": "off",
|
||||
// Should ignore due to mocks
|
||||
"signoz/no-navigator-clipboard": "off"
|
||||
// Tests can use navigator.clipboard directly
|
||||
"signoz/no-navigator-clipboard": "off",
|
||||
// Tests can use navigator.clipboard directly,
|
||||
"signoz/no-raw-absolute-path":"off",
|
||||
"no-restricted-globals": "off"
|
||||
// Tests need raw localStorage/sessionStorage to seed DOM state for isolation
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// oxlint-disable-next-line typescript/no-require-imports
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<base href="[[.BaseHref]]" />
|
||||
<meta
|
||||
http-equiv="Cache-Control"
|
||||
content="no-cache, no-store, must-revalidate, max-age: 0"
|
||||
@@ -39,12 +40,12 @@
|
||||
<meta
|
||||
data-react-helmet="true"
|
||||
property="og:image"
|
||||
content="/images/signoz-hero-image.webp"
|
||||
content="[[.BaseHref]]images/signoz-hero-image.webp"
|
||||
/>
|
||||
<meta
|
||||
data-react-helmet="true"
|
||||
name="twitter:image"
|
||||
content="/images/signoz-hero-image.webp"
|
||||
content="[[.BaseHref]]images/signoz-hero-image.webp"
|
||||
/>
|
||||
<meta
|
||||
data-react-helmet="true"
|
||||
@@ -59,7 +60,7 @@
|
||||
<meta data-react-helmet="true" name="docusaurus_locale" content="en" />
|
||||
<meta data-react-helmet="true" name="docusaurus_tag" content="default" />
|
||||
<meta name="robots" content="noindex" />
|
||||
<link data-react-helmet="true" rel="shortcut icon" href="/favicon.ico" />
|
||||
<link data-react-helmet="true" rel="shortcut icon" href="favicon.ico" />
|
||||
</head>
|
||||
<body data-theme="default">
|
||||
<script>
|
||||
@@ -67,8 +68,14 @@
|
||||
// Mirrors the logic in ThemeProvider (hooks/useDarkMode/index.tsx).
|
||||
(function () {
|
||||
try {
|
||||
var theme = localStorage.getItem('THEME');
|
||||
var autoSwitch = localStorage.getItem('THEME_AUTO_SWITCH') === 'true';
|
||||
// When served under a URL prefix (e.g. /signoz/), storage keys are scoped
|
||||
// to that prefix by the React app (see utils/storage.ts getScopedKey).
|
||||
// Read the <base> tag — already populated by the Go template — to derive
|
||||
// the same prefix here, before any JS module has loaded.
|
||||
var basePath = (document.querySelector('base') || {}).getAttribute('href') || '/';
|
||||
var prefix = basePath === '/' ? '' : basePath;
|
||||
var theme = localStorage.getItem(prefix + 'THEME');
|
||||
var autoSwitch = localStorage.getItem(prefix + 'THEME_AUTO_SWITCH') === 'true';
|
||||
if (autoSwitch) {
|
||||
theme = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
@@ -136,7 +143,7 @@
|
||||
})(document, 'script');
|
||||
}
|
||||
</script>
|
||||
<link rel="stylesheet" href="/css/uPlot.min.css" />
|
||||
<link rel="stylesheet" href="css/uPlot.min.css" />
|
||||
<script type="module" src="./src/index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
152
frontend/plugins/rules/no-raw-absolute-path.mjs
Normal file
152
frontend/plugins/rules/no-raw-absolute-path.mjs
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Rule: no-raw-absolute-path
|
||||
*
|
||||
* Catches patterns that break at runtime when the app is served from a
|
||||
* sub-path (e.g. /signoz/):
|
||||
*
|
||||
* 1. window.open(path, '_blank')
|
||||
* → use openInNewTab(path) which calls withBasePath internally
|
||||
*
|
||||
* 2. window.location.origin + path / `${window.location.origin}${path}`
|
||||
* → use getAbsoluteUrl(path)
|
||||
*
|
||||
* 3. frontendBaseUrl: window.location.origin (bare origin usage)
|
||||
* → use getBaseUrl() to include the base path
|
||||
*
|
||||
* 4. window.location.href = path
|
||||
* → use withBasePath(path) or navigate() for internal navigation
|
||||
*
|
||||
* External URLs (first arg starts with "http") are explicitly allowed.
|
||||
*/
|
||||
|
||||
function isOriginAccess(node) {
|
||||
return (
|
||||
node.type === 'MemberExpression' &&
|
||||
!node.computed &&
|
||||
node.property.name === 'origin' &&
|
||||
node.object.type === 'MemberExpression' &&
|
||||
!node.object.computed &&
|
||||
node.object.property.name === 'location' &&
|
||||
node.object.object.type === 'Identifier' &&
|
||||
node.object.object.name === 'window'
|
||||
);
|
||||
}
|
||||
|
||||
function isHrefAccess(node) {
|
||||
return (
|
||||
node.type === 'MemberExpression' &&
|
||||
!node.computed &&
|
||||
node.property.name === 'href' &&
|
||||
node.object.type === 'MemberExpression' &&
|
||||
!node.object.computed &&
|
||||
node.object.property.name === 'location' &&
|
||||
node.object.object.type === 'Identifier' &&
|
||||
node.object.object.name === 'window'
|
||||
);
|
||||
}
|
||||
|
||||
function isExternalUrl(node) {
|
||||
if (node.type === 'Literal' && typeof node.value === 'string') {
|
||||
return node.value.startsWith('http://') || node.value.startsWith('https://');
|
||||
}
|
||||
if (node.type === 'TemplateLiteral' && node.quasis.length > 0) {
|
||||
const raw = node.quasis[0].value.raw;
|
||||
return raw.startsWith('http://') || raw.startsWith('https://');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// window.open(withBasePath(x)) and window.open(getAbsoluteUrl(x)) are already safe.
|
||||
function isSafeHelperCall(node) {
|
||||
return (
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
(node.callee.name === 'withBasePath' || node.callee.name === 'getAbsoluteUrl')
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
type: 'suggestion',
|
||||
docs: {
|
||||
description:
|
||||
'Disallow raw window.open and origin-concatenation patterns that miss the runtime base path',
|
||||
category: 'Base Path Safety',
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
windowOpen:
|
||||
'Use openInNewTab(path) instead of window.open(path, "_blank") — openInNewTab prepends the base path automatically.',
|
||||
originConcat:
|
||||
'Use getAbsoluteUrl(path) instead of window.location.origin + path — getAbsoluteUrl prepends the base path automatically.',
|
||||
originDirect:
|
||||
'Use getBaseUrl() instead of window.location.origin — getBaseUrl includes the base path.',
|
||||
hrefAssign:
|
||||
'Use withBasePath(path) or navigate() instead of window.location.href = path — ensures the base path is included.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
return {
|
||||
// window.open(path, ...) — allow only external first-arg URLs
|
||||
CallExpression(node) {
|
||||
const { callee, arguments: args } = node;
|
||||
if (
|
||||
callee.type !== 'MemberExpression' ||
|
||||
callee.object.type !== 'Identifier' ||
|
||||
callee.object.name !== 'window' ||
|
||||
callee.property.name !== 'open'
|
||||
)
|
||||
{return;}
|
||||
if (args.length === 0) {return;}
|
||||
if (isExternalUrl(args[0])) {return;}
|
||||
if (isSafeHelperCall(args[0])) {return;}
|
||||
|
||||
context.report({ node, messageId: 'windowOpen' });
|
||||
},
|
||||
|
||||
// window.location.origin + path
|
||||
BinaryExpression(node) {
|
||||
if (node.operator !== '+') {return;}
|
||||
if (isOriginAccess(node.left) || isOriginAccess(node.right)) {
|
||||
context.report({ node, messageId: 'originConcat' });
|
||||
}
|
||||
},
|
||||
|
||||
// `${window.location.origin}${path}`
|
||||
TemplateLiteral(node) {
|
||||
if (node.expressions.some(isOriginAccess)) {
|
||||
context.report({ node, messageId: 'originConcat' });
|
||||
}
|
||||
},
|
||||
|
||||
// window.location.origin used directly (not in concatenation)
|
||||
// Catches: frontendBaseUrl: window.location.origin
|
||||
MemberExpression(node) {
|
||||
if (!isOriginAccess(node)) {return;}
|
||||
|
||||
const parent = node.parent;
|
||||
// Skip if parent is BinaryExpression with + (handled by BinaryExpression visitor)
|
||||
if (parent.type === 'BinaryExpression' && parent.operator === '+') {return;}
|
||||
// Skip if inside TemplateLiteral (handled by TemplateLiteral visitor)
|
||||
if (parent.type === 'TemplateLiteral') {return;}
|
||||
|
||||
context.report({ node, messageId: 'originDirect' });
|
||||
},
|
||||
|
||||
// window.location.href = path
|
||||
AssignmentExpression(node) {
|
||||
if (node.operator !== '=') {return;}
|
||||
if (!isHrefAccess(node.left)) {return;}
|
||||
|
||||
// Allow external URLs
|
||||
if (isExternalUrl(node.right)) {return;}
|
||||
// Allow safe helper calls
|
||||
if (isSafeHelperCall(node.right)) {return;}
|
||||
|
||||
context.report({ node, messageId: 'hrefAssign' });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -8,6 +8,7 @@
|
||||
import noZustandGetStateInHooks from './rules/no-zustand-getstate-in-hooks.mjs';
|
||||
import noNavigatorClipboard from './rules/no-navigator-clipboard.mjs';
|
||||
import noUnsupportedAssetPattern from './rules/no-unsupported-asset-pattern.mjs';
|
||||
import noRawAbsolutePath from './rules/no-raw-absolute-path.mjs';
|
||||
|
||||
export default {
|
||||
meta: {
|
||||
@@ -17,5 +18,6 @@ export default {
|
||||
'no-zustand-getstate-in-hooks': noZustandGetStateInHooks,
|
||||
'no-navigator-clipboard': noNavigatorClipboard,
|
||||
'no-unsupported-asset-pattern': noUnsupportedAssetPattern,
|
||||
'no-raw-absolute-path': noRawAbsolutePath,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { initReactI18next } from 'react-i18next';
|
||||
import i18n from 'i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import Backend from 'i18next-http-backend';
|
||||
import { getBasePath } from 'utils/basePath';
|
||||
|
||||
import cacheBursting from '../../i18n-translations-hash.json';
|
||||
|
||||
@@ -24,7 +25,7 @@ i18n
|
||||
const ns = namespace[0];
|
||||
const pathkey = `/${language}/${ns}`;
|
||||
const hash = cacheBursting[pathkey as keyof typeof cacheBursting] || '';
|
||||
return `/locales/${language}/${namespace}.json?h=${hash}`;
|
||||
return `${getBasePath()}locales/${language}/${namespace}.json?h=${hash}`;
|
||||
},
|
||||
},
|
||||
react: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
130
frontend/src/api/browser/localstorage/__tests__/get.test.ts
Normal file
130
frontend/src/api/browser/localstorage/__tests__/get.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* localstorage/get — lazy migration tests.
|
||||
*
|
||||
* basePath is memoized at module init, so each describe block re-imports the
|
||||
* module with a fresh DOM state via jest.isolateModules.
|
||||
*/
|
||||
|
||||
type GetModule = typeof import('../get');
|
||||
|
||||
function loadGetModule(href: string): GetModule {
|
||||
const base = document.createElement('base');
|
||||
base.setAttribute('href', href);
|
||||
document.head.append(base);
|
||||
|
||||
let mod!: GetModule;
|
||||
jest.isolateModules(() => {
|
||||
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
|
||||
mod = require('../get');
|
||||
});
|
||||
return mod;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const el of document.head.querySelectorAll('base')) {
|
||||
el.remove();
|
||||
}
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
describe('get — root path "/"', () => {
|
||||
it('reads the bare key', () => {
|
||||
const { default: get } = loadGetModule('/');
|
||||
localStorage.setItem('AUTH_TOKEN', 'tok');
|
||||
expect(get('AUTH_TOKEN')).toBe('tok');
|
||||
});
|
||||
|
||||
it('returns null when key is absent', () => {
|
||||
const { default: get } = loadGetModule('/');
|
||||
expect(get('MISSING')).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT promote bare keys (no-op at root)', () => {
|
||||
const { default: get } = loadGetModule('/');
|
||||
localStorage.setItem('THEME', 'light');
|
||||
get('THEME');
|
||||
// bare key must still be present — no migration at root
|
||||
expect(localStorage.getItem('THEME')).toBe('light');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get — prefixed path "/signoz/"', () => {
|
||||
it('reads an already-scoped key directly', () => {
|
||||
const { default: get } = loadGetModule('/signoz/');
|
||||
localStorage.setItem('/signoz/AUTH_TOKEN', 'scoped-tok');
|
||||
expect(get('AUTH_TOKEN')).toBe('scoped-tok');
|
||||
});
|
||||
|
||||
it('returns null when neither scoped nor bare key exists', () => {
|
||||
const { default: get } = loadGetModule('/signoz/');
|
||||
expect(get('MISSING')).toBeNull();
|
||||
});
|
||||
|
||||
it('lazy-migrates bare key to scoped key on first read', () => {
|
||||
const { default: get } = loadGetModule('/signoz/');
|
||||
localStorage.setItem('AUTH_TOKEN', 'old-tok');
|
||||
|
||||
const result = get('AUTH_TOKEN');
|
||||
|
||||
expect(result).toBe('old-tok');
|
||||
expect(localStorage.getItem('/signoz/AUTH_TOKEN')).toBe('old-tok');
|
||||
expect(localStorage.getItem('AUTH_TOKEN')).toBeNull();
|
||||
});
|
||||
|
||||
it('scoped key takes precedence over bare key', () => {
|
||||
const { default: get } = loadGetModule('/signoz/');
|
||||
localStorage.setItem('AUTH_TOKEN', 'bare-tok');
|
||||
localStorage.setItem('/signoz/AUTH_TOKEN', 'scoped-tok');
|
||||
|
||||
expect(get('AUTH_TOKEN')).toBe('scoped-tok');
|
||||
// bare key left untouched — scoped already existed
|
||||
expect(localStorage.getItem('AUTH_TOKEN')).toBe('bare-tok');
|
||||
});
|
||||
|
||||
it('subsequent reads after migration use scoped key (no double-write)', () => {
|
||||
const { default: get } = loadGetModule('/signoz/');
|
||||
localStorage.setItem('THEME', 'dark');
|
||||
|
||||
get('THEME'); // triggers migration
|
||||
localStorage.removeItem('THEME'); // simulate bare key gone
|
||||
|
||||
// second read still finds the scoped key
|
||||
expect(get('THEME')).toBe('dark');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get — two-prefix isolation', () => {
|
||||
it('/signoz/ and /testing/ do not share migrated values', () => {
|
||||
localStorage.setItem('THEME', 'light');
|
||||
|
||||
const base1 = document.createElement('base');
|
||||
base1.setAttribute('href', '/signoz/');
|
||||
document.head.append(base1);
|
||||
let getSignoz!: GetModule['default'];
|
||||
jest.isolateModules(() => {
|
||||
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
|
||||
getSignoz = require('../get').default;
|
||||
});
|
||||
base1.remove();
|
||||
|
||||
// migrate bare → /signoz/THEME
|
||||
getSignoz('THEME');
|
||||
|
||||
const base2 = document.createElement('base');
|
||||
base2.setAttribute('href', '/testing/');
|
||||
document.head.append(base2);
|
||||
let getTesting!: GetModule['default'];
|
||||
jest.isolateModules(() => {
|
||||
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
|
||||
getTesting = require('../get').default;
|
||||
});
|
||||
base2.remove();
|
||||
|
||||
// /testing/ prefix: bare key already gone, scoped key does not exist
|
||||
expect(getTesting('THEME')).toBeNull();
|
||||
expect(localStorage.getItem('/signoz/THEME')).toBe('light');
|
||||
expect(localStorage.getItem('/testing/THEME')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
@@ -1,7 +1,26 @@
|
||||
/* oxlint-disable no-restricted-globals */
|
||||
import { getBasePath } from 'utils/basePath';
|
||||
import { getScopedKey } from 'utils/storage';
|
||||
|
||||
const get = (key: string): string | null => {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
} catch (e) {
|
||||
const scopedKey = getScopedKey(key);
|
||||
const value = localStorage.getItem(scopedKey);
|
||||
|
||||
// Lazy migration: if running under a URL prefix and the scoped key doesn't
|
||||
// exist yet, fall back to the bare key (written by a previous root deployment).
|
||||
// Promote it to the scoped key and remove the bare key so future reads are fast.
|
||||
if (value === null && getBasePath() !== '/') {
|
||||
const bare = localStorage.getItem(key);
|
||||
if (bare !== null) {
|
||||
localStorage.setItem(scopedKey, bare);
|
||||
localStorage.removeItem(key);
|
||||
return bare;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/* oxlint-disable no-restricted-globals */
|
||||
import { getScopedKey } from 'utils/storage';
|
||||
|
||||
const remove = (key: string): boolean => {
|
||||
try {
|
||||
window.localStorage.removeItem(key);
|
||||
localStorage.removeItem(getScopedKey(key));
|
||||
return true;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/* oxlint-disable no-restricted-globals */
|
||||
import { getScopedKey } from 'utils/storage';
|
||||
|
||||
const set = (key: string, value: string): boolean => {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
localStorage.setItem(getScopedKey(key), value);
|
||||
return true;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* sessionstorage/get — lazy migration tests.
|
||||
* Mirrors the localStorage get tests; same logic, different storage.
|
||||
*/
|
||||
|
||||
type GetModule = typeof import('../get');
|
||||
|
||||
function loadGetModule(href: string): GetModule {
|
||||
const base = document.createElement('base');
|
||||
base.setAttribute('href', href);
|
||||
document.head.append(base);
|
||||
|
||||
let mod!: GetModule;
|
||||
jest.isolateModules(() => {
|
||||
// oxlint-disable-next-line typescript-eslint/no-require-imports, typescript-eslint/no-var-requires
|
||||
mod = require('../get');
|
||||
});
|
||||
return mod;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const el of document.head.querySelectorAll('base')) {
|
||||
el.remove();
|
||||
}
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
describe('get — root path "/"', () => {
|
||||
it('reads the bare key', () => {
|
||||
const { default: get } = loadGetModule('/');
|
||||
sessionStorage.setItem('retry-lazy-refreshed', 'true');
|
||||
expect(get('retry-lazy-refreshed')).toBe('true');
|
||||
});
|
||||
|
||||
it('returns null when key is absent', () => {
|
||||
const { default: get } = loadGetModule('/');
|
||||
expect(get('MISSING')).toBeNull();
|
||||
});
|
||||
|
||||
it('does NOT promote bare keys at root', () => {
|
||||
const { default: get } = loadGetModule('/');
|
||||
sessionStorage.setItem('retry-lazy-refreshed', 'true');
|
||||
get('retry-lazy-refreshed');
|
||||
expect(sessionStorage.getItem('retry-lazy-refreshed')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get — prefixed path "/signoz/"', () => {
|
||||
it('reads an already-scoped key directly', () => {
|
||||
const { default: get } = loadGetModule('/signoz/');
|
||||
sessionStorage.setItem('/signoz/retry-lazy-refreshed', 'true');
|
||||
expect(get('retry-lazy-refreshed')).toBe('true');
|
||||
});
|
||||
|
||||
it('returns null when neither scoped nor bare key exists', () => {
|
||||
const { default: get } = loadGetModule('/signoz/');
|
||||
expect(get('MISSING')).toBeNull();
|
||||
});
|
||||
|
||||
it('lazy-migrates bare key to scoped key on first read', () => {
|
||||
const { default: get } = loadGetModule('/signoz/');
|
||||
sessionStorage.setItem('retry-lazy-refreshed', 'true');
|
||||
|
||||
const result = get('retry-lazy-refreshed');
|
||||
|
||||
expect(result).toBe('true');
|
||||
expect(sessionStorage.getItem('/signoz/retry-lazy-refreshed')).toBe('true');
|
||||
expect(sessionStorage.getItem('retry-lazy-refreshed')).toBeNull();
|
||||
});
|
||||
|
||||
it('scoped key takes precedence over bare key', () => {
|
||||
const { default: get } = loadGetModule('/signoz/');
|
||||
sessionStorage.setItem('retry-lazy-refreshed', 'bare');
|
||||
sessionStorage.setItem('/signoz/retry-lazy-refreshed', 'scoped');
|
||||
|
||||
expect(get('retry-lazy-refreshed')).toBe('scoped');
|
||||
expect(sessionStorage.getItem('retry-lazy-refreshed')).toBe('bare');
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
27
frontend/src/api/browser/sessionstorage/get.ts
Normal file
27
frontend/src/api/browser/sessionstorage/get.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/* oxlint-disable no-restricted-globals */
|
||||
import { getBasePath } from 'utils/basePath';
|
||||
import { getScopedKey } from 'utils/storage';
|
||||
|
||||
const get = (key: string): string | null => {
|
||||
try {
|
||||
const scopedKey = getScopedKey(key);
|
||||
const value = sessionStorage.getItem(scopedKey);
|
||||
|
||||
// Lazy migration: same pattern as localStorage — promote bare keys written
|
||||
// by a previous root deployment to the scoped key on first read.
|
||||
if (value === null && getBasePath() !== '/') {
|
||||
const bare = sessionStorage.getItem(key);
|
||||
if (bare !== null) {
|
||||
sessionStorage.setItem(scopedKey, bare);
|
||||
sessionStorage.removeItem(key);
|
||||
return bare;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export default get;
|
||||
13
frontend/src/api/browser/sessionstorage/remove.ts
Normal file
13
frontend/src/api/browser/sessionstorage/remove.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/* oxlint-disable no-restricted-globals */
|
||||
import { getScopedKey } from 'utils/storage';
|
||||
|
||||
const remove = (key: string): boolean => {
|
||||
try {
|
||||
sessionStorage.removeItem(getScopedKey(key));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default remove;
|
||||
13
frontend/src/api/browser/sessionstorage/set.ts
Normal file
13
frontend/src/api/browser/sessionstorage/set.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/* oxlint-disable no-restricted-globals */
|
||||
import { getScopedKey } from 'utils/storage';
|
||||
|
||||
const set = (key: string, value: string): boolean => {
|
||||
try {
|
||||
sessionStorage.setItem(getScopedKey(key), value);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default set;
|
||||
@@ -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 };
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -5559,6 +5559,237 @@ export interface TelemetrytypesTelemetryFieldValuesDTO {
|
||||
|
||||
export type TimeDurationDTO = number;
|
||||
|
||||
export type TracedetailtypesEventDTOAttributeMap = { [key: string]: unknown };
|
||||
|
||||
export interface TracedetailtypesEventDTO {
|
||||
/**
|
||||
* @type object
|
||||
*/
|
||||
attributeMap?: TracedetailtypesEventDTOAttributeMap;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
isError?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
timeUnixNano?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type TracedetailtypesGettableWaterfallTraceDTOServiceNameToTotalDurationMap =
|
||||
{ [key: string]: number } | null;
|
||||
|
||||
export interface TracedetailtypesGettableWaterfallTraceDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
endTimestampMillis?: number;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
hasMissingSpans?: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
hasMore?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
rootServiceEntryPoint?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
rootServiceName?: string;
|
||||
/**
|
||||
* @type object
|
||||
* @nullable true
|
||||
*/
|
||||
serviceNameToTotalDurationMap?: TracedetailtypesGettableWaterfallTraceDTOServiceNameToTotalDurationMap;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
spans?: TracedetailtypesWaterfallSpanDTO[] | null;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
startTimestampMillis?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
totalErrorSpansCount?: number;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
totalSpansCount?: number;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
uncollapsedSpans?: string[] | null;
|
||||
}
|
||||
|
||||
export interface TracedetailtypesPostableWaterfallDTO {
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
limit?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
selectedSpanId?: string;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
uncollapsedSpans?: string[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type TracedetailtypesWaterfallSpanDTOAttributes = {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type TracedetailtypesWaterfallSpanDTOResource = {
|
||||
[key: string]: string;
|
||||
} | null;
|
||||
|
||||
export interface TracedetailtypesWaterfallSpanDTO {
|
||||
/**
|
||||
* @type object
|
||||
* @nullable true
|
||||
*/
|
||||
attributes?: TracedetailtypesWaterfallSpanDTOAttributes;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
db_name?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
db_operation?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
duration_nano?: number;
|
||||
/**
|
||||
* @type array
|
||||
* @nullable true
|
||||
*/
|
||||
events?: TracedetailtypesEventDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
external_http_method?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
external_http_url?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
flags?: number;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
has_children?: boolean;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
has_error?: boolean;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
http_host?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
http_method?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
http_url?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
is_remote?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
kind_string?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
level?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
parent_span_id?: string;
|
||||
/**
|
||||
* @type object
|
||||
* @nullable true
|
||||
*/
|
||||
resource?: TracedetailtypesWaterfallSpanDTOResource;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
response_status_code?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
span_id?: string;
|
||||
/**
|
||||
* @type integer
|
||||
*/
|
||||
status_code?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status_code_string?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status_message?: string;
|
||||
/**
|
||||
* @type integer
|
||||
* @minimum 0
|
||||
*/
|
||||
sub_tree_node_count?: number;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
trace_id?: string;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
trace_state?: string;
|
||||
}
|
||||
|
||||
export interface TypesAlertStatusDTO {
|
||||
/**
|
||||
* @type array
|
||||
@@ -7493,6 +7724,17 @@ export type GetHosts200 = {
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetWaterfallPathParameters = {
|
||||
traceID: string;
|
||||
};
|
||||
export type GetWaterfall200 = {
|
||||
data: TracedetailtypesGettableWaterfallTraceDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type QueryRangeV5200 = {
|
||||
data: Querybuildertypesv5QueryRangeResponseDTO;
|
||||
/**
|
||||
|
||||
123
frontend/src/api/generated/services/tracedetail/index.ts
Normal file
123
frontend/src/api/generated/services/tracedetail/index.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* ! Do not edit manually
|
||||
* * The file has been auto-generated using Orval for SigNoz
|
||||
* * regenerate with 'yarn generate:api'
|
||||
* SigNoz
|
||||
*/
|
||||
import { useMutation } from 'react-query';
|
||||
import type {
|
||||
MutationFunction,
|
||||
UseMutationOptions,
|
||||
UseMutationResult,
|
||||
} from 'react-query';
|
||||
|
||||
import type {
|
||||
GetWaterfall200,
|
||||
GetWaterfallPathParameters,
|
||||
RenderErrorResponseDTO,
|
||||
TracedetailtypesPostableWaterfallDTO,
|
||||
} from '../sigNoz.schemas';
|
||||
|
||||
import { GeneratedAPIInstance } from '../../../generatedAPIInstance';
|
||||
import type { ErrorType, BodyType } from '../../../generatedAPIInstance';
|
||||
|
||||
/**
|
||||
* Returns the waterfall view of spans for a given trace ID with tree structure, metadata, and windowed pagination
|
||||
* @summary Get waterfall view for a trace
|
||||
*/
|
||||
export const getWaterfall = (
|
||||
{ traceID }: GetWaterfallPathParameters,
|
||||
tracedetailtypesPostableWaterfallDTO: BodyType<TracedetailtypesPostableWaterfallDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<GetWaterfall200>({
|
||||
url: `/api/v3/traces/${traceID}/waterfall`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: tracedetailtypesPostableWaterfallDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getGetWaterfallMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof getWaterfall>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data: BodyType<TracedetailtypesPostableWaterfallDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof getWaterfall>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data: BodyType<TracedetailtypesPostableWaterfallDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['getWaterfall'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof getWaterfall>>,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data: BodyType<TracedetailtypesPostableWaterfallDTO>;
|
||||
}
|
||||
> = (props) => {
|
||||
const { pathParams, data } = props ?? {};
|
||||
|
||||
return getWaterfall(pathParams, data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type GetWaterfallMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof getWaterfall>>
|
||||
>;
|
||||
export type GetWaterfallMutationBody =
|
||||
BodyType<TracedetailtypesPostableWaterfallDTO>;
|
||||
export type GetWaterfallMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Get waterfall view for a trace
|
||||
*/
|
||||
export const useGetWaterfall = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof getWaterfall>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data: BodyType<TracedetailtypesPostableWaterfallDTO>;
|
||||
},
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof getWaterfall>>,
|
||||
TError,
|
||||
{
|
||||
pathParams: GetWaterfallPathParameters;
|
||||
data: BodyType<TracedetailtypesPostableWaterfallDTO>;
|
||||
},
|
||||
TContext
|
||||
> => {
|
||||
const mutationOptions = getGetWaterfallMutationOptions(options);
|
||||
|
||||
return useMutation(mutationOptions);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
interceptorRejected,
|
||||
interceptorsRequestBasePath,
|
||||
interceptorsRequestResponse,
|
||||
interceptorsResponse,
|
||||
} from 'api';
|
||||
@@ -17,7 +18,9 @@ export const GeneratedAPIInstance = <T>(
|
||||
return generatedAPIAxiosInstance({ ...config }).then(({ data }) => data);
|
||||
};
|
||||
|
||||
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
|
||||
generatedAPIAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
generatedAPIAxiosInstance.interceptors.response.use(
|
||||
interceptorsResponse,
|
||||
interceptorRejected,
|
||||
|
||||
@@ -11,6 +11,7 @@ import axios, {
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { Events } from 'constants/events';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { getBasePath } from 'utils/basePath';
|
||||
import { eventEmitter } from 'utils/getEventEmitter';
|
||||
|
||||
import apiV1, { apiAlertManager, apiV2, apiV3, apiV4, apiV5 } from './apiV1';
|
||||
@@ -31,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, {
|
||||
@@ -54,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
|
||||
});
|
||||
|
||||
@@ -67,6 +68,39 @@ export const interceptorsRequestResponse = (
|
||||
return value;
|
||||
};
|
||||
|
||||
// Strips the leading '/' from path and joins with base — idempotent if already prefixed.
|
||||
// e.g. prependBase('/signoz/', '/api/v1/') → '/signoz/api/v1/'
|
||||
function prependBase(base: string, path: string): string {
|
||||
return path.startsWith(base) ? path : base + path.slice(1);
|
||||
}
|
||||
|
||||
// Prepends the runtime base path to outgoing requests so API calls work under
|
||||
// a URL prefix (e.g. /signoz/api/v1/…). No-op for root deployments and dev
|
||||
// (dev baseURL is a full http:// URL, not an absolute path).
|
||||
export const interceptorsRequestBasePath = (
|
||||
value: InternalAxiosRequestConfig,
|
||||
): InternalAxiosRequestConfig => {
|
||||
const basePath = getBasePath();
|
||||
if (basePath === '/') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value.baseURL?.startsWith('/')) {
|
||||
// Production relative baseURL: '/api/v1/' → '/signoz/api/v1/'
|
||||
value.baseURL = prependBase(basePath, value.baseURL);
|
||||
} else if (value.baseURL?.startsWith('http')) {
|
||||
// Dev absolute baseURL (VITE_FRONTEND_API_ENDPOINT): 'https://host/api/v1/' → 'https://host/signoz/api/v1/'
|
||||
const url = new URL(value.baseURL);
|
||||
url.pathname = prependBase(basePath, url.pathname);
|
||||
value.baseURL = url.toString();
|
||||
} else if (!value.baseURL && value.url?.startsWith('/')) {
|
||||
// Orval-generated client (empty baseURL, path in url): '/api/signoz/v1/rules' → '/signoz/api/signoz/v1/rules'
|
||||
value.url = prependBase(basePath, value.url);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export const interceptorRejected = async (
|
||||
value: AxiosResponse<any>,
|
||||
): Promise<AxiosResponse<any>> => {
|
||||
@@ -109,7 +143,7 @@ export const interceptorRejected = async (
|
||||
Logout();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
} catch {
|
||||
Logout();
|
||||
}
|
||||
}
|
||||
@@ -126,13 +160,14 @@ 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}`,
|
||||
});
|
||||
|
||||
instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
instance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
instance.interceptors.response.use(interceptorsResponse, interceptorRejected);
|
||||
|
||||
export const AxiosAlertManagerInstance = axios.create({
|
||||
@@ -147,6 +182,7 @@ ApiV2Instance.interceptors.response.use(
|
||||
interceptorRejected,
|
||||
);
|
||||
ApiV2Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
ApiV2Instance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
|
||||
// axios V3
|
||||
export const ApiV3Instance = axios.create({
|
||||
@@ -158,6 +194,7 @@ ApiV3Instance.interceptors.response.use(
|
||||
interceptorRejected,
|
||||
);
|
||||
ApiV3Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
ApiV3Instance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
//
|
||||
|
||||
// axios V4
|
||||
@@ -170,6 +207,7 @@ ApiV4Instance.interceptors.response.use(
|
||||
interceptorRejected,
|
||||
);
|
||||
ApiV4Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
ApiV4Instance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
//
|
||||
|
||||
// axios V5
|
||||
@@ -182,6 +220,7 @@ ApiV5Instance.interceptors.response.use(
|
||||
interceptorRejected,
|
||||
);
|
||||
ApiV5Instance.interceptors.request.use(interceptorsRequestResponse);
|
||||
ApiV5Instance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
//
|
||||
|
||||
// axios Base
|
||||
@@ -194,6 +233,7 @@ LogEventAxiosInstance.interceptors.response.use(
|
||||
interceptorRejectedBase,
|
||||
);
|
||||
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestResponse);
|
||||
LogEventAxiosInstance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
//
|
||||
|
||||
AxiosAlertManagerInstance.interceptors.response.use(
|
||||
@@ -201,6 +241,7 @@ AxiosAlertManagerInstance.interceptors.response.use(
|
||||
interceptorRejected,
|
||||
);
|
||||
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestResponse);
|
||||
AxiosAlertManagerInstance.interceptors.request.use(interceptorsRequestBasePath);
|
||||
|
||||
export { apiV1 };
|
||||
export default instance;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -3,13 +3,16 @@ import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import { ENVIRONMENT } from 'constants/env';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { EventSourcePolyfill } from 'event-source-polyfill';
|
||||
import { withBasePath } from 'utils/basePath';
|
||||
|
||||
// 10 min in ms
|
||||
const TIMEOUT_IN_MS = 10 * 60 * 1000;
|
||||
|
||||
export const LiveTail = (queryParams: string): EventSourcePolyfill =>
|
||||
new EventSourcePolyfill(
|
||||
`${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`,
|
||||
ENVIRONMENT.baseURL
|
||||
? `${ENVIRONMENT.baseURL}${apiV1}logs/tail?${queryParams}`
|
||||
: withBasePath(`${apiV1}logs/tail?${queryParams}`),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${getLocalStorageKey(LOCALSTORAGE.AUTH_TOKEN)}`,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -85,7 +85,7 @@ function convertTimeSeriesData(
|
||||
const { index, alias } = aggregation;
|
||||
const seriesData = aggregation[seriesKey];
|
||||
|
||||
if (!seriesData || !seriesData.length) {
|
||||
if (!seriesData || seriesData.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -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: '' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 />);
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -12,6 +12,7 @@ import { AppState } from 'store/reducers';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, MetricAggregateOperator } from 'types/common/queryBuilder';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { withBasePath } from 'utils/basePath';
|
||||
|
||||
export interface NavigateToExplorerProps {
|
||||
filters: TagFilterItem[];
|
||||
@@ -133,7 +134,7 @@ export function useNavigateToExplorer(): (
|
||||
QueryParams.compositeQuery
|
||||
}=${JSONCompositeQuery}`;
|
||||
|
||||
window.open(newExplorerPath, sameTab ? '_self' : '_blank');
|
||||
window.open(withBasePath(newExplorerPath), sameTab ? '_self' : '_blank');
|
||||
},
|
||||
[
|
||||
prepareQuery,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { CreditCard, MessageSquareText, X } from 'lucide-react';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import APIError from 'types/api/error';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
|
||||
export default function ChatSupportGateway(): JSX.Element {
|
||||
const { notifications } = useNotifications();
|
||||
@@ -54,7 +55,7 @@ export default function ChatSupportGateway(): JSX.Element {
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: getBaseUrl(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -21,7 +21,7 @@ interface RangePickerModalProps {
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
onCustomDateHandler: (
|
||||
dateTimeRange: DateTimeRangeType,
|
||||
lexicalContext?: LexicalContext | undefined,
|
||||
lexicalContext?: LexicalContext ,
|
||||
) => void;
|
||||
selectedTime: string;
|
||||
onTimeChange?: (
|
||||
|
||||
@@ -82,7 +82,7 @@ const createTimezoneEntry = (
|
||||
name: displayName,
|
||||
value,
|
||||
offset,
|
||||
searchIndex: offset.replace(/ /g, ''),
|
||||
searchIndex: offset.replaceAll(/ /g, ''),
|
||||
...(hasDivider && { hasDivider }),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,7 @@ import { useAppContext } from 'providers/App/App';
|
||||
import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import { useTimezone } from 'providers/Timezone';
|
||||
import APIError from 'types/api/error';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
import { toAPIError } from 'utils/errorUtils';
|
||||
|
||||
import DeleteMemberDialog from './DeleteMemberDialog';
|
||||
@@ -381,7 +382,7 @@ function EditMemberDrawer({
|
||||
pathParams: { id: member.id },
|
||||
});
|
||||
if (response?.data?.token) {
|
||||
const link = `${window.location.origin}/password-reset?token=${response.data.token}`;
|
||||
const link = getAbsoluteUrl(`/password-reset?token=${response.data.token}`);
|
||||
setResetLink(link);
|
||||
setResetLinkExpiresAt(
|
||||
response.data.expiresAt
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -13,6 +13,7 @@ import GetMinMax from 'lib/getMinMax';
|
||||
import { Check, Info, Link2 } from 'lucide-react';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
import { getAbsoluteUrl } from 'utils/basePath';
|
||||
|
||||
const routesToBeSharedWithTime = [
|
||||
ROUTES.LOGS_EXPLORER,
|
||||
@@ -80,17 +81,13 @@ function ShareURLModal(): JSX.Element {
|
||||
|
||||
urlQuery.delete(QueryParams.relativeTime);
|
||||
|
||||
currentUrl = `${window.location.origin}${
|
||||
location.pathname
|
||||
}?${urlQuery.toString()}`;
|
||||
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
|
||||
} else {
|
||||
urlQuery.delete(QueryParams.startTime);
|
||||
urlQuery.delete(QueryParams.endTime);
|
||||
|
||||
urlQuery.set(QueryParams.relativeTime, selectedTime);
|
||||
currentUrl = `${window.location.origin}${
|
||||
location.pathname
|
||||
}?${urlQuery.toString()}`;
|
||||
currentUrl = getAbsoluteUrl(`${location.pathname}?${urlQuery.toString()}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useErrorModal } from 'providers/ErrorModalProvider';
|
||||
import APIError from 'types/api/error';
|
||||
import { ROLES } from 'types/roles';
|
||||
import { EMAIL_REGEX } from 'utils/app';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
import { popupContainer } from 'utils/selectPopupContainer';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -191,7 +192,7 @@ function InviteMembersModal({
|
||||
email: row.email.trim(),
|
||||
name: '',
|
||||
role: row.role as ROLES,
|
||||
frontendBaseUrl: window.location.origin,
|
||||
frontendBaseUrl: getBaseUrl(),
|
||||
});
|
||||
} else {
|
||||
await inviteUsers({
|
||||
@@ -199,7 +200,7 @@ function InviteMembersModal({
|
||||
email: row.email.trim(),
|
||||
name: '',
|
||||
role: row.role,
|
||||
frontendBaseUrl: window.location.origin,
|
||||
frontendBaseUrl: getBaseUrl(),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useAppContext } from 'providers/App/App';
|
||||
import { SuccessResponseV2 } from 'types/api';
|
||||
import { CheckoutSuccessPayloadProps } from 'types/api/billing/checkout';
|
||||
import APIError from 'types/api/error';
|
||||
import { getBaseUrl } from 'utils/basePath';
|
||||
|
||||
import './LaunchChatSupport.styles.scss';
|
||||
|
||||
@@ -154,7 +155,7 @@ function LaunchChatSupport({
|
||||
});
|
||||
|
||||
updateCreditCard({
|
||||
url: window.location.origin,
|
||||
url: getBaseUrl(),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button } from 'antd';
|
||||
import { ArrowUpRight } from 'lucide-react';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
|
||||
import './LearnMore.styles.scss';
|
||||
|
||||
@@ -14,7 +15,7 @@ function LearnMore({ text, url, onClick }: LearnMoreProps): JSX.Element {
|
||||
const handleClick = (): void => {
|
||||
onClick?.();
|
||||
if (url) {
|
||||
window.open(url, '_blank');
|
||||
openInNewTab(url);
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
KAFKA_SETUP_DOC_LINK,
|
||||
MessagingQueueHealthCheckService,
|
||||
} from 'pages/MessagingQueues/MessagingQueuesUtils';
|
||||
import { openInNewTab } from 'utils/navigation';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import './MessagingQueueHealthCheck.styles.scss';
|
||||
@@ -76,7 +77,7 @@ function ErrorTitleAndKey({
|
||||
if (isCloudUserVal && !!link) {
|
||||
history.push(link);
|
||||
} else {
|
||||
window.open(KAFKA_SETUP_DOC_LINK, '_blank');
|
||||
openInNewTab(KAFKA_SETUP_DOC_LINK);
|
||||
}
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />,
|
||||
);
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -25,7 +25,7 @@ function OverlayScrollbar({
|
||||
autoHide: 'scroll',
|
||||
theme: isDarkMode ? 'os-theme-light' : 'os-theme-dark',
|
||||
},
|
||||
...(customOptions || {}),
|
||||
...customOptions,
|
||||
} as PartialOptions),
|
||||
[customOptions, isDarkMode],
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '' });
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import getSessionStorageApi from 'api/browser/sessionstorage/get';
|
||||
import removeSessionStorageApi from 'api/browser/sessionstorage/remove';
|
||||
import setSessionStorageApi from 'api/browser/sessionstorage/set';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
export const PREVIOUS_QUERY_KEY = 'previousQuery';
|
||||
|
||||
function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
|
||||
try {
|
||||
const raw = sessionStorage.getItem(PREVIOUS_QUERY_KEY);
|
||||
const raw = getSessionStorageApi(PREVIOUS_QUERY_KEY);
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
@@ -17,7 +20,7 @@ function getPreviousQueryFromStore(): Record<string, IBuilderQuery> {
|
||||
|
||||
function writePreviousQueryToStore(store: Record<string, IBuilderQuery>): void {
|
||||
try {
|
||||
sessionStorage.setItem(PREVIOUS_QUERY_KEY, JSON.stringify(store));
|
||||
setSessionStorageApi(PREVIOUS_QUERY_KEY, JSON.stringify(store));
|
||||
} catch {
|
||||
// ignore quota or serialization errors
|
||||
}
|
||||
@@ -63,7 +66,7 @@ export const removeKeyFromPreviousQuery = (key: string): void => {
|
||||
|
||||
export const clearPreviousQuery = (): void => {
|
||||
try {
|
||||
sessionStorage.removeItem(PREVIOUS_QUERY_KEY);
|
||||
removeSessionStorageApi(PREVIOUS_QUERY_KEY);
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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()',
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -139,7 +139,7 @@ function Duration({
|
||||
attribute as AllTraceFilterKeys,
|
||||
)
|
||||
) {
|
||||
if (!values || !values.length) {
|
||||
if (!values || values.length === 0) {
|
||||
return [];
|
||||
}
|
||||
let minValue = '';
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(' ');
|
||||
|
||||
@@ -24,7 +24,7 @@ function RefreshPaymentStatus({
|
||||
try {
|
||||
await refreshPaymentStatus();
|
||||
|
||||
await Promise.all([activeLicenseRefetch()]);
|
||||
[await activeLicenseRefetch()];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
@@ -108,8 +108,7 @@ function DynamicColumnTable({
|
||||
// Update URL with new page number while preserving other params
|
||||
urlQuery.set('page', page.toString());
|
||||
|
||||
const newUrl = `${window.location.pathname}?${urlQuery.toString()}`;
|
||||
safeNavigate(newUrl);
|
||||
safeNavigate({ search: `?${urlQuery.toString()}` });
|
||||
|
||||
// Call original pagination handler if provided
|
||||
if (pagination?.onChange && !!pageSize) {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import getLocalStorageKey from 'api/browser/localstorage/get';
|
||||
import setLocalStorageKey from 'api/browser/localstorage/set';
|
||||
|
||||
import { DynamicColumnsKey } from './contants';
|
||||
import {
|
||||
GetNewColumnDataFunction,
|
||||
@@ -12,7 +15,7 @@ export const getVisibleColumns: GetVisibleColumnsFunction = ({
|
||||
}) => {
|
||||
let columnVisibilityData: { [key: string]: boolean };
|
||||
try {
|
||||
const storedData = localStorage.getItem(tablesource);
|
||||
const storedData = getLocalStorageKey(tablesource);
|
||||
if (typeof storedData === 'string' && dynamicColumns) {
|
||||
columnVisibilityData = JSON.parse(storedData);
|
||||
return dynamicColumns.filter((column) => {
|
||||
@@ -28,7 +31,7 @@ export const getVisibleColumns: GetVisibleColumnsFunction = ({
|
||||
initialColumnVisibility[key] = false;
|
||||
});
|
||||
|
||||
localStorage.setItem(tablesource, JSON.stringify(initialColumnVisibility));
|
||||
setLocalStorageKey(tablesource, JSON.stringify(initialColumnVisibility));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -42,14 +45,14 @@ export const setVisibleColumns = ({
|
||||
dynamicColumns,
|
||||
}: SetVisibleColumnsProps): void => {
|
||||
try {
|
||||
const storedData = localStorage.getItem(tablesource);
|
||||
const storedData = getLocalStorageKey(tablesource);
|
||||
if (typeof storedData === 'string' && dynamicColumns) {
|
||||
const columnVisibilityData = JSON.parse(storedData);
|
||||
const { key } = dynamicColumns[index];
|
||||
if (key) {
|
||||
columnVisibilityData[key] = checked;
|
||||
}
|
||||
localStorage.setItem(tablesource, JSON.stringify(columnVisibilityData));
|
||||
setLocalStorageKey(tablesource, JSON.stringify(columnVisibilityData));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -65,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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user